Je souhaite tronquer du texte (chargé à partir d'une base de données ou d'un fichier texte), mais il contient du code HTML. Par conséquent, les balises sont incluses et moins de texte est renvoyé. Cela peut alors avoir pour conséquence que les balises ne soient pas fermées ou soient partiellement fermées (de sorte que Tidy risque de ne pas fonctionner correctement et qu'il y ait encore moins de contenu). Comment puis-je tronquer en fonction du texte (et probablement m'arrêter lorsque vous arrivez à une table car cela pourrait causer des problèmes plus complexes).
substr("Hello, my <strong>name</strong> is <em>Sam</em>. I´m a web developer.",0,26)."..."
Aboutirait à:
Hello, my <strong>name</st...
Ce que je voudrais, c'est:
Hello, my <strong>name</strong> is <em>Sam</em>. I´m...
Comment puis-je faire ceci?
Bien que ma question concerne la façon de le faire en PHP, il serait bon de savoir comment le faire en C # ... soit ça devrait aller, car je pense que je pourrais transférer la méthode (à moins que ce méthode).
Notez également que j'ai inclus une entité HTML ´
- qui devrait être considérée comme un seul caractère (plutôt que 7 caractères comme dans cet exemple).
strip_tags
est un repli, mais je perdrais la mise en forme et les liens et le problème persisterait avec les entités HTML.
En supposant que vous utilisez du XHTML valide, il est simple d'analyser le code HTML et de vous assurer que les balises sont gérées correctement. Vous devez simplement suivre les balises qui ont été ouvertes jusqu'à présent et assurez-vous de les refermer "à votre sortie".
<?php
header('Content-type: text/plain; charset=utf-8');
function printTruncated($maxLength, $html, $isUtf8=true)
{
$printedLength = 0;
$position = 0;
$tags = array();
// For UTF-8, we need to count multibyte sequences as one character.
$re = $isUtf8
? '{</?([a-z]+)[^>]*>|&#?[a-zA-Z0-9]+;|[\x80-\xFF][\x80-\xBF]*}'
: '{</?([a-z]+)[^>]*>|&#?[a-zA-Z0-9]+;}';
while ($printedLength < $maxLength && preg_match($re, $html, $match, PREG_OFFSET_CAPTURE, $position))
{
list($tag, $tagPosition) = $match[0];
// Print text leading up to the tag.
$str = substr($html, $position, $tagPosition - $position);
if ($printedLength + strlen($str) > $maxLength)
{
print(substr($str, 0, $maxLength - $printedLength));
$printedLength = $maxLength;
break;
}
print($str);
$printedLength += strlen($str);
if ($printedLength >= $maxLength) break;
if ($tag[0] == '&' || ord($tag) >= 0x80)
{
// Pass the entity or UTF-8 multibyte sequence through unchanged.
print($tag);
$printedLength++;
}
else
{
// Handle the tag.
$tagName = $match[1][0];
if ($tag[1] == '/')
{
// This is a closing tag.
$openingTag = array_pop($tags);
assert($openingTag == $tagName); // check that tags are properly nested.
print($tag);
}
else if ($tag[strlen($tag) - 2] == '/')
{
// Self-closing tag.
print($tag);
}
else
{
// Opening tag.
print($tag);
$tags[] = $tagName;
}
}
// Continue after the tag.
$position = $tagPosition + strlen($tag);
}
// Print any remaining text.
if ($printedLength < $maxLength && $position < strlen($html))
print(substr($html, $position, $maxLength - $printedLength));
// Close any open tags.
while (!empty($tags))
printf('</%s>', array_pop($tags));
}
printTruncated(10, '<b><Hello></b> <img src="world.png" alt="" /> world!'); print("\n");
printTruncated(10, '<table><tr><td>Heck, </td><td>throw</td></tr><tr><td>in a</td><td>table</td></tr></table>'); print("\n");
printTruncated(10, "<em><b>Hello</b>w\xC3\xB8rld!</em>"); print("\n");
Note de codage : Le code ci-dessus suppose que le XHTML est UTF-8 codé. Les codages à un octet compatibles ASCII (tels que Latin-1 ) sont également pris en charge, il suffit de passer false
en tant que troisième argument. Les autres codages multi-octets ne sont pas pris en charge, bien que vous puissiez modifier le support en utilisant mb_convert_encoding
pour convertir en UTF-8 avant d'appeler la fonction, puis en reconvertissant à chaque instruction print
.
(Vous devriez cependant toujours utiliser UTF-8.)
Edit : mis à jour pour gérer les entités de caractères et UTF-8. Correction d'un bug où la fonction imprimait un caractère de trop, si ce caractère était une entité de personnage.
J'ai écrit une fonction qui tronque le code HTML comme vous le suggérez, mais au lieu de l’imprimer, elle conserve simplement le tout dans une variable chaîne. gère également les entités HTML.
/**
* function to truncate and then clean up end of the HTML,
* truncates by counting characters outside of HTML tags
*
* @author alex lockwood, alex dot lockwood at websightdesign
*
* @param string $str the string to truncate
* @param int $len the number of characters
* @param string $end the end string for truncation
* @return string $truncated_html
*
* **/
public static function truncateHTML($str, $len, $end = '…'){
//find all tags
$tagPattern = '/(<\/?)([\w]*)(\s*[^>]*)>?|&[\w#]+;/i'; //match html tags and entities
preg_match_all($tagPattern, $str, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER );
//WSDDebug::dump($matches); exit;
$i =0;
//loop through each found tag that is within the $len, add those characters to the len,
//also track open and closed tags
// $matches[$i][0] = the whole tag string --the only applicable field for html enitities
// IF its not matching an &htmlentity; the following apply
// $matches[$i][1] = the start of the tag either '<' or '</'
// $matches[$i][2] = the tag name
// $matches[$i][3] = the end of the tag
//$matces[$i][$j][0] = the string
//$matces[$i][$j][1] = the str offest
while($matches[$i][0][1] < $len && !empty($matches[$i])){
$len = $len + strlen($matches[$i][0][0]);
if(substr($matches[$i][0][0],0,1) == '&' )
$len = $len-1;
//if $matches[$i][2] is undefined then its an html entity, want to ignore those for tag counting
//ignore empty/singleton tags for tag counting
if(!empty($matches[$i][2][0]) && !in_array($matches[$i][2][0],array('br','img','hr', 'input', 'param', 'link'))){
//double check
if(substr($matches[$i][3][0],-1) !='/' && substr($matches[$i][1][0],-1) !='/')
$openTags[] = $matches[$i][2][0];
elseif(end($openTags) == $matches[$i][2][0]){
array_pop($openTags);
}else{
$warnings[] = "html has some tags mismatched in it: $str";
}
}
$i++;
}
$closeTags = '';
if (!empty($openTags)){
$openTags = array_reverse($openTags);
foreach ($openTags as $t){
$closeTagString .="</".$t . ">";
}
}
if(strlen($str)>$len){
// Finds the last space from the string new length
$lastWord = strpos($str, ' ', $len);
if ($lastWord) {
//truncate with new len last Word
$str = substr($str, 0, $lastWord);
//finds last character
$last_character = (substr($str, -1, 1));
//add the end text
$truncated_html = ($last_character == '.' ? $str : ($last_character == ',' ? substr($str, 0, -1) : $str) . $end);
}
//restore any open tags
$truncated_html .= $closeTagString;
}else
$truncated_html = $str;
return $truncated_html;
}
J'ai utilisé une fonction Nice trouvée sur http://alanwhipple.com/2011/05/25/php-truncate-string-preserving-html-tags-words , apparemment tirée de CakePHP.
Approche précise à 100%, mais assez difficile:
Approche facile par force brute:
preg_split('/(<tag>)/')
avec PREG_DELIM_CAPTURE.html_entity_decode()
pour vous aider à mesurer avec précision)&[^\s;]+$
à la fin pour vous débarrasser de l'entité éventuellement hachée)Dans ce cas, vous pouvez utiliser DomDocument avec un bidouillage de regex, le pire qui puisse arriver est un avertissement, s'il y a une balise cassée:
$dom = new DOMDocument();
$dom->loadHTML(substr("Hello, my <strong>name</strong> is <em>Sam</em>. I´m a web developer.",0,26));
$html = preg_replace("/\<\/?(body|html|p)>/", "", $dom->saveHTML());
echo $html;
Devrait donner la sortie: Hello, my <strong>**name**</strong>
.
Bounce a ajouté la prise en charge des caractères multi-octets à la solution de Søren Løvborg - j'ai ajouté:
<hr>
, <br>
, <col>
, etc. ne sont pas fermés - en HTML, un '/' n'est pas requis à la fin de celles-ci (dans le cas où XHTML est utilisé),&hellips;
i.e.…),Tout ça chez Pastie .
Une autre lumière passe à la fonction printTruncated de Søren Løvborg, la rendant compatible UTF-8 (Needs mbstring) et rendant la chaîne renvoyée non imprimée. Je pense que c'est plus utile. Et mon code n'utilise pas la mise en mémoire tampon comme la variante Bounce, juste une variable supplémentaire.
UPD: pour que cela fonctionne correctement avec les caractères utf-8 dans les attributs de balises, vous avez besoin de la fonction mb_preg_match, listée ci-dessous.
Merci beaucoup à Søren Løvborg pour cette fonction, c'est très bien.
/* Truncate HTML, close opened tags
*
* @param int, maxlength of the string
* @param string, html
* @return $html
*/
function htmlTruncate($maxLength, $html)
{
mb_internal_encoding("UTF-8");
$printedLength = 0;
$position = 0;
$tags = array();
$out = "";
while ($printedLength < $maxLength && mb_preg_match('{</?([a-z]+)[^>]*>|&#?[a-zA-Z0-9]+;}', $html, $match, PREG_OFFSET_CAPTURE, $position))
{
list($tag, $tagPosition) = $match[0];
// Print text leading up to the tag.
$str = mb_substr($html, $position, $tagPosition - $position);
if ($printedLength + mb_strlen($str) > $maxLength)
{
$out .= mb_substr($str, 0, $maxLength - $printedLength);
$printedLength = $maxLength;
break;
}
$out .= $str;
$printedLength += mb_strlen($str);
if ($tag[0] == '&')
{
// Handle the entity.
$out .= $tag;
$printedLength++;
}
else
{
// Handle the tag.
$tagName = $match[1][0];
if ($tag[1] == '/')
{
// This is a closing tag.
$openingTag = array_pop($tags);
assert($openingTag == $tagName); // check that tags are properly nested.
$out .= $tag;
}
else if ($tag[mb_strlen($tag) - 2] == '/')
{
// Self-closing tag.
$out .= $tag;
}
else
{
// Opening tag.
$out .= $tag;
$tags[] = $tagName;
}
}
// Continue after the tag.
$position = $tagPosition + mb_strlen($tag);
}
// Print any remaining text.
if ($printedLength < $maxLength && $position < mb_strlen($html))
$out .= mb_substr($html, $position, $maxLength - $printedLength);
// Close any open tags.
while (!empty($tags))
$out .= sprintf('</%s>', array_pop($tags));
return $out;
}
function mb_preg_match(
$ps_pattern,
$ps_subject,
&$pa_matches,
$pn_flags = 0,
$pn_offset = 0,
$ps_encoding = NULL
) {
// WARNING! - All this function does is to correct offsets, nothing else:
//(code is independent of PREG_PATTER_ORDER / PREG_SET_ORDER)
if (is_null($ps_encoding)) $ps_encoding = mb_internal_encoding();
$pn_offset = strlen(mb_substr($ps_subject, 0, $pn_offset, $ps_encoding));
$ret = preg_match($ps_pattern, $ps_subject, $pa_matches, $pn_flags, $pn_offset);
if ($ret && ($pn_flags & PREG_OFFSET_CAPTURE))
foreach($pa_matches as &$ha_match) {
$ha_match[1] = mb_strlen(substr($ps_subject, 0, $ha_match[1]), $ps_encoding);
}
return $ret;
}
Le CakePHP framework a une fonction truncate () compatible HTML dans TextHelper qui fonctionne pour moi. Voir Core-Helpers/Text . MIT licence.
vous pouvez utiliser ranger aussi:
function truncate_html($html, $max_length) {
return tidy_repair_string(substr($html, 0, $max_length),
array('wrap' => 0, 'show-body-only' => TRUE), 'utf8');
}
J'ai apporté de légères modifications à la fonction Søren Løvborg printTruncated
en la rendant compatible UTF-8:
/* Truncate HTML, close opened tags
*
* @param int, maxlength of the string
* @param string, html
* @return $html
*/
function html_truncate($maxLength, $html){
mb_internal_encoding("UTF-8");
$printedLength = 0;
$position = 0;
$tags = array();
ob_start();
while ($printedLength < $maxLength && preg_match('{</?([a-z]+)[^>]*>|&#?[a-zA-Z0-9]+;}', $html, $match, PREG_OFFSET_CAPTURE, $position)){
list($tag, $tagPosition) = $match[0];
// Print text leading up to the tag.
$str = mb_strcut($html, $position, $tagPosition - $position);
if ($printedLength + mb_strlen($str) > $maxLength){
print(mb_strcut($str, 0, $maxLength - $printedLength));
$printedLength = $maxLength;
break;
}
print($str);
$printedLength += mb_strlen($str);
if ($tag[0] == '&'){
// Handle the entity.
print($tag);
$printedLength++;
}
else{
// Handle the tag.
$tagName = $match[1][0];
if ($tag[1] == '/'){
// This is a closing tag.
$openingTag = array_pop($tags);
assert($openingTag == $tagName); // check that tags are properly nested.
print($tag);
}
else if ($tag[mb_strlen($tag) - 2] == '/'){
// Self-closing tag.
print($tag);
}
else{
// Opening tag.
print($tag);
$tags[] = $tagName;
}
}
// Continue after the tag.
$position = $tagPosition + mb_strlen($tag);
}
// Print any remaining text.
if ($printedLength < $maxLength && $position < mb_strlen($html))
print(mb_strcut($html, $position, $maxLength - $printedLength));
// Close any open tags.
while (!empty($tags))
printf('</%s>', array_pop($tags));
$bufferOuput = ob_get_contents();
ob_end_clean();
$html = $bufferOuput;
return $html;
}
Ce qui suit est un analyseur de machine d'état simple qui gère votre cas de test avec succès. Je ne parviens pas sur les balises imbriquées car il ne suit pas les balises elles-mêmes. J'étouffe aussi les entités dans les balises HTML (par exemple, dans un attribut href
- d'une balise <a>
-). Cela ne peut donc pas être considéré comme une solution à 100% à ce problème mais, comme il est facile à comprendre, cela pourrait servir de base à une fonction plus avancée.
function substr_html($string, $length)
{
$count = 0;
/*
* $state = 0 - normal text
* $state = 1 - in HTML tag
* $state = 2 - in HTML entity
*/
$state = 0;
for ($i = 0; $i < strlen($string); $i++) {
$char = $string[$i];
if ($char == '<') {
$state = 1;
} else if ($char == '&') {
$state = 2;
$count++;
} else if ($char == ';') {
$state = 0;
} else if ($char == '>') {
$state = 0;
} else if ($state === 0) {
$count++;
}
if ($count === $length) {
return substr($string, 0, $i + 1);
}
}
return $string;
}
Ceci est très difficile à faire sans utiliser un validateur et un analyseur, la raison étant que vous imaginez si vous avez
<div id='x'>
<div id='y'>
<h1>Heading</h1>
500
lines
of
html
...
etc
...
</div>
</div>
Comment envisagez-vous de tronquer cela et de vous retrouver avec un code HTML valide?
Après une brève recherche, j'ai trouvé ce lien qui pourrait aider.
Utilisez la fonction truncateHTML()
de: Https://github.com/jlgrall/truncateHTML
Exemple: tronque après 9 caractères, y compris les points de suspension:
truncateHTML(9, "<p><b>A</b> red ball.</p>", ['wholeWord' => false]);
// => "<p><b>A</b> red ba…</p>"
Caractéristiques: UTF-8, Ellipsis configurables, inclure/exclure la longueur des Ellipsis, balises à fermeture automatique, espaces réductibles, éléments invisibles (<head>
, <script>
, <noscript>
, <style>
, <!-- comments -->
), HTML $entities;
, tronqué enfin Mot entier (avec la possibilité de toujours tronquer des mots très longs), PHP 5.6 et 7.0+, plus de 240 tests unitaires, renvoie une chaîne (n'utilise pas le tampon de sortie) et un code bien commenté.
J'ai écrit cette fonction, parce que j'aimais beaucoup la fonction de Søren Løvborg ci-dessus (en particulier la façon dont il gérait les encodages), mais il me fallait un peu plus de fonctionnalités et de flexibilité.