Zkrácení textu s XHTML značkami

Školení, která pořádám

Článek vyšel v rámci PHP okénka na serveru Root.cz.

Pokud chceme vypsat pouze začátek nějakého textu, není v PHP nic jednoduššího:

<?php
echo substr($text, 0, $limit);
?>

Pokud text nechceme ukončovat uprostřed slova, tak to také není dvakrát velká věda:

<?php
if (strlen($text) <= $limit) {
    echo $text;
} else {
    $text = substr($text, 0, $limit+1);
    $pos = strrpos($text, " "); // v PHP 5 by se dal použít parametr offset
    echo substr($text, 0, ($pos ? $pos : -1)) . "...";
}
?>

Funkce strrpos vrátí v případě nenalezení podřetězce false a v případě nalezení na samém začátku řetězce 0, mezi čímž je obvykle potřebovat pomocí operátoru === rozlišovat, my ale oba případy ošetříme stejně – vypsáním řetězce zkráceného přesně na požadovanou délku.

Co ale dělat v případě, kdy $text obsahuje HTML značky? Pokud nám na nich nezáleží, dá se jednoduše použít funkce strip_tags. Pokud je ale v textu chceme zachovat, musíme si napsat funkci vlastní. Mohla by být založena na regulárních výrazech, nejjednodušší ale bude použít staré dobré procházení po znacích:

<?php
/** Zkrácení textu s XHTML značkami
* @param string zkracovaný řetězec bez komentářů a bloků skriptu
* @param int požadovaný počet vrácených znaků
* @return string zkrácený řetězec se správně uzavřenými značkami
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
function xhtml_cut_tidy($s, $limit) {
    $length = 0;
    for ($i=0; $i < strlen($s) && $length < $limit; $i++) {
        switch ($s[$i]) {
        case '<':
            $in_quote = '';
            while ($i < strlen($s) && ($in_quote || $s[$i] != '>')) {
                if (($s[$i] == '"' || $s[$i] == "'") && !$in_quote) {
                    $in_quote = $s[$i];
                } elseif ($in_quote == $s[$i]) {
                    $in_quote = '';
                }
                $i++;
            }
            break;
        case '&':
            $length++;
            while ($i < strlen($s) && $s[$i] != ';') {
                $i++;
            }
            break;
        default:
            $length++;
        }
    }
    $config = array('output-xhtml' => true, 'show-body-only' => true);
    return tidy_repair_string(substr($s, 0, $i), $config, 'raw');
}
?>

Funkce prochází řetězec, značky a entity přeskakuje a započítává pouze zobrazované znaky. Zkrácený řetězec ošetří funkcí tidy_repair_string, která doplní chybějící uzavírací značky. Pokud bychom ošetřovali HTML řetězec, museli bychom kromě nepředání parametru output-xhtml funkci tidy_repair_string upravit také zpracování entit, které v HTML není tak přísné.

Funkce tidy_repair_string nám ušetřila spoustu práce, jak ale postupovat v případě, že není k dispozici? Bude potřeba ukládat otevírané značky do zásobníku a na závěr je uzavřít:

<?php
/** Zkrácení textu s XHTML značkami
* @param string zkracovaný řetězec bez komentářů a bloků skriptu
* @param int požadovaný počet vrácených znaků
* @return string zkrácený řetězec se správně uzavřenými značkami
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
function xhtml_cut($s, $limit) {
    $length = 0;
    $tags = array(); // dosud neuzavřené značky
    for ($i=0; $i < strlen($s) && $length < $limit; $i++) {
        switch ($s[$i]) {
        
        case '<':
            // načtení značky
            $start = $i+1;
            while ($i < strlen($s) && $s[$i] != '>' && !ctype_space($s[$i])) {
                $i++;
            }
            $tag = substr($s, $start, $i - $start);
            // přeskočení případných atributů
            $in_quote = '';
            while ($i < strlen($s) && ($in_quote || $s[$i] != '>')) {
                if (($s[$i] == '"' || $s[$i] == "'") && !$in_quote) {
                    $in_quote = $s[$i];
                } elseif ($in_quote == $s[$i]) {
                    $in_quote = '';
                }
                $i++;
            }
            if ($s[$start] == '/') { // uzavírací značka
                array_shift($tags); // v XHTML dokumentu musí být vždy uzavřena poslední neuzavřená značka
            } elseif ($s[$i-1] != '/') { // otevírací značka
                array_unshift($tags, $tag);
            }
            break;
        
        case '&':
            $length++;
            while ($i < strlen($s) && $s[$i] != ';') {
                $i++;
            }
            break;
        
        default:
            $length++;
            /* V případě kódování UTF-8:
            while ($i+1 < strlen($s) && ord($s[$i+1]) > 127 && ord($s[$i+1]) < 192) {
                $i++;
            }        
            */
        
        }
    }
    $s = substr($s, 0, $i);
    if ($tags) {
        $s .= "</" . implode("></", $tags) . ">";
    }
    return $s;
}
?>

Úprava na tolerantnější HTML verzi by v tomto případě byla o něco složitější, protože v HTML nemusí být nepárové značky označeny a navíc některé značky uzavřeny být mohou, ale nemusí. V případě zpracování textu v kódování UTF-8 je možné ve větvi default použít zakomentovaný kód, který přeskočí vícebajtové znaky.

Pokud si zadavatel usmyslí, že chce za všech okolností zobrazovat např. 4 řádky, dá se postupovat tak, že se text zkrátí s dostatečnou rezervou a na čtyři řádky se seřízne u klienta pomocí stylu height: 5em; overflow: hidden; line-height: 1.25;, kde hodnota u height je požadovaný počet řádek vynásobený hodnotou line-height v jednotkách em. V HTML5 lze navíc použít text-overflow: ellipsis.

Jakub Vrána, Řešení problému, 20.6.2005, diskuse: 14 (nové: 0)

Diskuse

ikona Jakub Vrána OpenID:

HTML verze:

<?php
/** Zkrácení textu s HTML značkami
* @param string $s zkracovaný řetězec bez komentářů a bloků skriptu
* @param int $limit požadovaný počet vrácených znaků
* @return string zkrácený řetězec se správně uzavřenými značkami
* @copyright Jakub Vrána, http://php.vrana.cz
*/
function html_cut($s, $limit)
{
    static $empty_tags = array('area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param');
    $length = 0;
    $tags = array(); // dosud neuzavřené značky
    for ($i=0; $i < strlen($s) && $length < $limit; $i++) {
        switch ($s{$i}) {

        case '<':
            // načtení značky
            $start = $i+1;
            while ($i < strlen($s) && $s{$i} != '>' && !ctype_space($s{$i})) {
                $i++;
            }
            $tag = strtolower(substr($s, $start, $i - $start));
            // přeskočení případných atributů
            $in_quote = '';
            while ($i < strlen($s) && ($in_quote || $s{$i} != '>')) {
                if (($s{$i} == '"' || $s{$i} == "'") && !$in_quote) {
                    $in_quote = $s{$i};
                } elseif ($in_quote == $s{$i}) {
                    $in_quote = '';
                }
                $i++;
            }
            if ($s{$start} == '/') { // uzavírací značka
                $tags = array_slice($tags, array_search(substr($tag, 1), $tags) + 1);
            } elseif ($s{$i-1} != '/' && !in_array($tag, $empty_tags)) { // otevírací značka
                array_unshift($tags, $tag);
            }
            break;

        case '&':
            $length++;
            while ($i < strlen($s) && $s{$i} != ';') {
                $i++;
            }
            break;

        default:
            $length++;

        }
    }
    $s = substr($s, 0, $i);
    if ($tags) {
        $s .= "</" . implode("></", $tags) . ">";
    }
    return $s;
}
?>

schimpanze:

co znamená proměnná $limit v prvním příkazu? je to snad nějaké omezení?

Pája:

DÍKY!!!!!
ať už hledám odpověď na jakýkoli PHP problém, na php.vrna.cz ji najdu skoro vždycky :)

ikona Mackiee:

Zdravim,
malinko jsem funkci upravil, aby v pripade, ze se retezec zkrati, pridal na konec tri tecky:
<?php
/** Zkrácení textu s HTML značkami
* @param string $s zkracovaný řetězec bez komentářů a bloků skriptu
* @param int $limit požadovaný počet vrácených znaků
* @return string zkrácený řetězec se správně uzavřenými značkami
* @copyright Jakub Vrána, http://php.vrana.cz
*/
function orizni_html($s, $limit){
    static $empty_tags = array('area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param');
    $length = 0;
    $tags = array(); // dosud neuzavřené značky

    if(strlen($s) > $limit){
      $tri_tecky = "...";
    } else {
      $tri_tecky = "";
    }

    for ($i=0; $i < strlen($s) && $length < $limit; $i++) {

        switch ($s{$i}) {

        case '<':
            // načtení značky
            $start = $i+1;
            while ($i < strlen($s) && $s{$i} != '>' && !ctype_space($s{$i})) {
                $i++;
            }
            $tag = strtolower(substr($s, $start, $i - $start));
            // přeskočení případných atributů
            $in_quote = '';
            while ($i < strlen($s) && ($in_quote || $s{$i} != '>')) {
                if (($s{$i} == '"' || $s{$i} == "'") && !$in_quote) {
                    $in_quote = $s{$i};
                } elseif ($in_quote == $s{$i}) {
                    $in_quote = '';
                }
                $i++;
            }
            if ($s{$start} == '/') { // uzavírací značka
                $tags = array_slice($tags, array_search(substr($tag, 1), $tags) + 1);
            } elseif ($s{$i-1} != '/' && !in_array($tag, $empty_tags)) { // otevírací značka
                array_unshift($tags, $tag);
            }
            break;

        case '&':
            $length++;
            while ($i < strlen($s) && $s{$i} != ';') {
                $i++;
            }
            break;

        default:
            $length++;

        }
    }
    $s = substr($s, 0, $i);
    $s .= $tri_tecky;
    if ($tags) {
        $s .= "</" . implode("></", $tags) . ">";
    }

    return $s;
}
?>

Prakticke by bylo, kdyby se script dal (napr. volitelnym parametrem) upravit tak, aby nerezal slova v pulce, ale jen na konci slova...  Ma nekdo nejaky napad?

Mike:

Takže tady je funkce, která zároveň i ořezává text až za celým slovem a zároveň přidá " [...]" za konec výsledného řetězce. Je to původní funkce pana Jakuba Vrány, xhtml verze.

<?php
function xhtml_cut($s, $limit) {
    // zde je v podstatě to, co můžete nalézt hned na začátku tohoto článku :)
    $s = substr($s, 0, $limit+1); // hlavní je, aby zde byl limit stejný jako ten globálně nastavený, což tedy nyní je...
    $pos = strrpos($s, " "); // v PHP 5 by se dal použít parametr offset
    $s = substr($s, 0, ($pos ? $pos : -1));

    $length = 0;
    $tags = array(); // dosud neuzavřené značky
    for ($i=0; $i < strlen($s) && $length < $limit; $i++) {
        switch ($s[$i]) {

        case '<':
            // načtení značky
            $start = $i+1;
            while ($i < strlen($s) && $s[$i] != '>' && !ctype_space($s[$i])) {
                $i++;
            }
            $tag = substr($s, $start, $i - $start);
            // přeskočení případných atributů
            $in_quote = '';
            while ($i < strlen($s) && ($in_quote || $s[$i] != '>')) {
                if (($s[$i] == '"' || $s[$i] == "'") && !$in_quote) {
                    $in_quote = $s[$i];
                } elseif ($in_quote == $s[$i]) {
                    $in_quote = '';
                }
                $i++;
            }
            if ($s[$start] == '/') { // uzavírací značka
                array_shift($tags); // v XHTML dokumentu musí být vždy uzavřena poslední neuzavřená značka
            } elseif ($s[$i-1] != '/') { // otevírací značka
                array_unshift($tags, $tag);
            }
            break;

        case '&':
            $length++;
            while ($i < strlen($s) && $s[$i] != ';') {
                $i++;
            }
            break;

        default:
            $length++;
            /* V případě kódování UTF-8:
            while ($i+1 < strlen($s) && ord($s[$i+1]) > 127 && ord($s[$i+1]) < 192) {
                $i++;
            }
            */

        }
    }
    $s = substr($s, 0, $i);
    if ($tags) {
        $s .= "</" . implode("></", $tags) . ">";
    }
    $s .= " [...]";
    return $s;
}
?>

ikona Konopek:

Přikládám upravenou funkci, která by mohla splňovat uvedené požadavky...

Konopek:

Tak jeste jeden pokus o prilozeni kodu: <?PHP
 
function html_cut($s, $limit,$whole_word=1){
    static $empty_tags = array('area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param');
    $length = 0;
    $tags = array(); //dosud neuzavrene znacky

    for($i=0; ($i < strlen($s) && $length < $limit) || ($whole_word==1 && ($length >= $limit && (isset($s{$i}) && !preg_match('/\s/',$s{$i})))); $i++) {
        switch ($s{$i}) {
        case '<':
        //nactení znacky
          $start = $i+1;
          while ($i < strlen($s) && $s{$i} != '>' && !ctype_space($s{$i})) {
            $i++;
          }#end while
          $tag = strtolower(substr($s, $start, $i - $start));
          //preskoení pripadnych atributu
            $in_quote = '';
            while ($i < strlen($s) && ($in_quote || $s{$i} != '>')) {
              if (($s{$i} == '"' || $s{$i} == "'") && !$in_quote) {
                $in_quote = $s{$i};
              }#end if
              elseif ($in_quote == $s{$i}) {
                $in_quote = '';
              }#end elseif
              $i++;
            }#end while
            if ($s{$start} == '/') {//uzaviraci znacka
              $tags = array_slice($tags, array_search(substr($tag, 1), $tags) + 1);
            }#end if
            elseif ($s{$i-1} != '/' && !in_array($tag, $empty_tags)) { //oteviraci znacka
              array_unshift($tags, $tag);
            }#end elseif
            break;
        case '&':
            $length++;
            while ($i < strlen($s) && $s{$i} != ';') {
                $i++;
            }#end while
            break;
        default:
            $length++;
        }#end switch
    }#end for
    $s = substr($s, 0, $i);
  //odstraneni prazdnych znaku na konci retezce
    $s=preg_replace('/[\s]$/is','',$s);
  //pokud neni konec retezce uzavreni tagu nebo nejaky znak na konci vety, pridej tri tecky
    if(!preg_match('/>[\s]{0,}$/is',$s) && !preg_match('/[\.!\?]$/is',$s)){$s .= '...';}
  //uzavreni tagu
    if ($tags) {
        $s .= "</" . implode("></", $tags) . ">";
    }#end if
  //navratova hodnota funkce
    return $s;
  }#end function

?>

WaOER:

Pozrite si funkciu

function html_cut($s, $limit)

A ono to skrati text na $limit aj pekne s HTML tagami, teda nebudu orezane alebo
nieco podobne...Je to super!

Len neviem ako tam mam zakonponovat, aby to orezalo od 100. znaku po 300. znak
(teda nie od 0)

Chapete ze?

Dakujem za radu.

Hacky:

Ja bych se chtěl zeptat jak mám vypsat zkrácený text kromě prvních 2 funkcí mi to nefunguje html tidy na serveru nemám takže zbyla poslední možnost a nevím jak ji vypsat.
Předem díky za odpověď.

Lukáš Gavenda:

Pokud chcete tuto funkci použít pro řetezce v kodóvání UTF-8, tak je třeba nahradit všechny fce: strlen() a substr() na mb_strlen() a mb_substr() a pojede to...

Lukáš Gavenda:

sry - prehlidnul jsem ten koment uvnitr fce, takze nevermind ;)

ikona Jakub Bouček:

Pro text bez HTML značek používám obyčejnou fci:

<?php
function shorttext($text, $limit=NULL, $treshold=NULL, $uncutwordlimit=0) {
  if($treshold === NULL) $treshold = 5;

  if($limit && mb_strlen($text, 'UTF-8')>($limit+$treshold)) {
    if($uncutwordlimit) {
      $pos = mb_strpos($text, ' ', $limit, 'UTF-8');
      if($pos && $pos<=($limit+$uncutwordlimit))
        $limit = $pos;
    }

    return mb_substr($text, 0, $limit, 'UTF-8') . '...';
  }
  return $text;
}
?>

$text - text ke zkrácení znaků
$treshold - počet znaků, které jsou tolerovány (aby to neřezalo znaky z věty, která je pouze o dva znaky delší než limit - hloupé)
$uncutwordlimit - počet znaků, které jsou tolerovány jako slovo

Pořadí argumentů je dáno historickým vývojem funkce.

Pro funkci je třeba rozšíření mbstring, ale zato nemá problém s kódováním UTF-8 (aby vícebitový znak nesekl v půlce).

Je třeba počítat s tím, že délka výstupního textu může překročit $limit až o max($treshold, $uncutwordlimit);

ikona jwkk:

Super! Děkuji! Moc jste mi všeci pomohli. Ukázka implementace zde http://hrejsi.jwkk.biz/en/gemcraft-chapter-one-the-forgotten dole v popisu :-)

smonkey:

Zdravím,
bojuji s takovou věcí. Vypisuji text, pomocí $data['text'], ve skriptech se používá $text, čili ho zaměním, jenže to vyhodí chybovou hlášku, to samé když si $text předem deklaruji.

Jak z této záludné situace? O.o Předem děkuji!

Vložit příspěvek

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2016 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.