Add ellipsis to end of truncated string in WordPress plugin - php

I'm trying to add an ellipsis to the end of the truncated strings this php function produces. It's part of a Wordpress plugin. But alas, I'm just learning php....
I've tried printing the ellipsis right before the ending html tag near the end of the function, like this
$truncate .= '... </' . $tag . '>';
but no luck. Any ideas? It must be fairly straightforward.
Here's the whole function:
if( !function_exists( 'yd_cake_truncate' ) ) {
function yd_cake_truncate($text, $length = 100, $ending = '...', $exact = true, $considerHtml = false) {
if ($considerHtml) {
// if the plain text is shorter than the maximum length, return the whole text
if (strlen(preg_replace('/<.*?>/', '', $text)) <= $length) {
return $text;
}
// splits all html-tags to scanable lines
preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
$total_length = strlen($ending);
$open_tags = array();
$truncate = '';
foreach ($lines as $line_matchings) {
// if there is any html-tag in this line, handle it and add it (uncounted) to the output
if (!empty($line_matchings[1])) {
// if it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>)
if (preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $line_matchings[1])) {
// do nothing
// if tag is a closing tag (f.e. </b>)
} else if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $line_matchings[1], $tag_matchings)) {
// delete tag from $open_tags list
$pos = array_search($tag_matchings[1], $open_tags);
if ($pos !== false) {
unset($open_tags[$pos]);
}
// if tag is an opening tag (f.e. <b>)
} else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $line_matchings[1], $tag_matchings)) {
// add tag to the beginning of $open_tags list
array_unshift($open_tags, strtolower($tag_matchings[1]));
}
// add html-tag to $truncate'd text
$truncate .= $line_matchings[1];
}
// calculate the length of the plain text part of the line; handle entities as one character
$content_length = strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $line_matchings[2]));
if ($total_length+$content_length> $length) {
// the number of characters which are left
$left = $length - $total_length;
$entities_length = 0;
// search for html entities
if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', $line_matchings[2], $entities, PREG_OFFSET_CAPTURE)) {
// calculate the real length of all entities in the legal range
foreach ($entities[0] as $entity) {
if ($entity[1]+1-$entities_length <= $left) {
$left--;
$entities_length += strlen($entity[0]);
} else {
// no more characters left
break;
}
}
}
$truncate .= substr($line_matchings[2], 0, $left+$entities_length);
// maximum lenght is reached, so get off the loop
break;
} else {
$truncate .= $line_matchings[2];
$total_length += $content_length;
}
// if the maximum length is reached, get off the loop
if($total_length>= $length) {
break;
}
}
} else {
if (strlen($text) <= $length) {
return $text;
} else {
$truncate = substr($text, 0, $length - strlen($ending));
}
}
// if the words shouldn't be cut in the middle...
if (!$exact) {
// ...search the last occurance of a space...
$spacepos = strrpos($truncate, ' ');
if (isset($spacepos)) {
// ...and cut the text in this position
$truncate = substr($truncate, 0, $spacepos);
}
}
// add the defined ending to the text
$truncate .= $ending;
if($considerHtml) {
// close all unclosed html-tags
foreach ($open_tags as $tag) {
$truncate .= '</' . $tag . '>';
}
}
return $truncate;
}
}

The function signature looks like it adds ellipses to the end of the truncated string by default using the $ending argument:
yd_cake_truncate($text, $length = 100, $ending = '...', $exact = true, $considerHtml = false)
Looking down further, $ending is appended to the truncated string near the end of the function:
// add the defined ending to the text
$truncate .= $ending;
How are you calling this function when you're trying to use it?

Related

Changed formation while use character limit in TBS library [duplicate]

I have various HTML strings to cut to 100 characters (of the stripped content, not the original) without stripping tags and without breaking HTML.
Original HTML string (288 characters):
$content = "<div>With a <span class='spanClass'>span over here</span> and a
<div class='divClass'>nested div over <div class='nestedDivClass'>there</div>
</div> and a lot of other nested <strong><em>texts</em> and tags in the air
<span>everywhere</span>, it's a HTML taggy kind of day.</strong></div>";
Standard trim: Trim to 100 characters and HTML breaks, stripped content comes to ~40 characters:
$content = substr($content, 0, 100)."..."; /* output:
<div>With a <span class='spanClass'>span over here</span> and a
<div class='divClass'>nested div ove... */
Stripped HTML: Outputs correct character count but obviously looses formatting:
$content = substr(strip_tags($content)), 0, 100)."..."; /* output:
With a span over here and a nested div over there and a lot of other nested
texts and tags in the ai... */
Partial solution: using HTML Tidy or purifier to close off tags outputs clean HTML but 100 characters of HTML not displayed content.
$content = substr($content, 0, 100)."...";
$tidy = new tidy; $tidy->parseString($content); $tidy->cleanRepair(); /* output:
<div>With a <span class='spanClass'>span over here</span> and a
<div class='divClass'>nested div ove</div></div>... */
Challenge: To output clean HTML and n characters (excluding character count of HTML elements):
$content = cutHTML($content, 100); /* output:
<div>With a <span class='spanClass'>span over here</span> and a
<div class='divClass'>nested div over <div class='nestedDivClass'>there</div>
</div> and a lot of other nested <strong><em>texts</em> and tags in the
ai</strong></div>...";
Similar Questions
How to clip HTML fragments without breaking up tags
Cutting HTML strings without breaking HTML tags
Not amazing, but works.
function html_cut($text, $max_length)
{
$tags = array();
$result = "";
$is_open = false;
$grab_open = false;
$is_close = false;
$in_double_quotes = false;
$in_single_quotes = false;
$tag = "";
$i = 0;
$stripped = 0;
$stripped_text = strip_tags($text);
while ($i < strlen($text) && $stripped < strlen($stripped_text) && $stripped < $max_length)
{
$symbol = $text{$i};
$result .= $symbol;
switch ($symbol)
{
case '<':
$is_open = true;
$grab_open = true;
break;
case '"':
if ($in_double_quotes)
$in_double_quotes = false;
else
$in_double_quotes = true;
break;
case "'":
if ($in_single_quotes)
$in_single_quotes = false;
else
$in_single_quotes = true;
break;
case '/':
if ($is_open && !$in_double_quotes && !$in_single_quotes)
{
$is_close = true;
$is_open = false;
$grab_open = false;
}
break;
case ' ':
if ($is_open)
$grab_open = false;
else
$stripped++;
break;
case '>':
if ($is_open)
{
$is_open = false;
$grab_open = false;
array_push($tags, $tag);
$tag = "";
}
else if ($is_close)
{
$is_close = false;
array_pop($tags);
$tag = "";
}
break;
default:
if ($grab_open || $is_close)
$tag .= $symbol;
if (!$is_open && !$is_close)
$stripped++;
}
$i++;
}
while ($tags)
$result .= "</".array_pop($tags).">";
return $result;
}
Usage example:
$content = html_cut($content, 100);
I'm not claiming to have invented this, but there is a very complete Text::truncate() method in CakePHP which does what you want:
function truncate($text, $length = 100, $ending = '...', $exact = true, $considerHtml = false) {
if (is_array($ending)) {
extract($ending);
}
if ($considerHtml) {
if (mb_strlen(preg_replace('/<.*?>/', '', $text)) <= $length) {
return $text;
}
$totalLength = mb_strlen($ending);
$openTags = array();
$truncate = '';
preg_match_all('/(<\/?([\w+]+)[^>]*>)?([^<>]*)/', $text, $tags, PREG_SET_ORDER);
foreach ($tags as $tag) {
if (!preg_match('/img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param/s', $tag[2])) {
if (preg_match('/<[\w]+[^>]*>/s', $tag[0])) {
array_unshift($openTags, $tag[2]);
} else if (preg_match('/<\/([\w]+)[^>]*>/s', $tag[0], $closeTag)) {
$pos = array_search($closeTag[1], $openTags);
if ($pos !== false) {
array_splice($openTags, $pos, 1);
}
}
}
$truncate .= $tag[1];
$contentLength = mb_strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $tag[3]));
if ($contentLength + $totalLength > $length) {
$left = $length - $totalLength;
$entitiesLength = 0;
if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', $tag[3], $entities, PREG_OFFSET_CAPTURE)) {
foreach ($entities[0] as $entity) {
if ($entity[1] + 1 - $entitiesLength <= $left) {
$left--;
$entitiesLength += mb_strlen($entity[0]);
} else {
break;
}
}
}
$truncate .= mb_substr($tag[3], 0 , $left + $entitiesLength);
break;
} else {
$truncate .= $tag[3];
$totalLength += $contentLength;
}
if ($totalLength >= $length) {
break;
}
}
} else {
if (mb_strlen($text) <= $length) {
return $text;
} else {
$truncate = mb_substr($text, 0, $length - strlen($ending));
}
}
if (!$exact) {
$spacepos = mb_strrpos($truncate, ' ');
if (isset($spacepos)) {
if ($considerHtml) {
$bits = mb_substr($truncate, $spacepos);
preg_match_all('/<\/([a-z]+)>/', $bits, $droppedTags, PREG_SET_ORDER);
if (!empty($droppedTags)) {
foreach ($droppedTags as $closingTag) {
if (!in_array($closingTag[1], $openTags)) {
array_unshift($openTags, $closingTag[1]);
}
}
}
}
$truncate = mb_substr($truncate, 0, $spacepos);
}
}
$truncate .= $ending;
if ($considerHtml) {
foreach ($openTags as $tag) {
$truncate .= '</'.$tag.'>';
}
}
return $truncate;
}
Use PHP's DOMDocument class to normalize an HTML fragment:
$dom= new DOMDocument();
$dom->loadHTML('<div><p>Hello World');
$xpath = new DOMXPath($dom);
$body = $xpath->query('/html/body');
echo($dom->saveXml($body->item(0)));
This question is similar to an earlier question and I've copied and pasted one solution here. If the HTML is submitted by users you'll also need to filter out potential Javascript attack vectors like onmouseover="do_something_evil()" or .... Tools like HTML Purifier were designed to catch and solve these problems and are far more comprehensive than any code that I could post.
I made another function to do it, it supports UTF-8:
/**
* Limit string without break html tags.
* Supports UTF8
*
* #param string $value
* #param int $limit Default 100
*/
function str_limit_html($value, $limit = 100)
{
if (mb_strwidth($value, 'UTF-8') <= $limit) {
return $value;
}
// Strip text with HTML tags, sum html len tags too.
// Is there another way to do it?
do {
$len = mb_strwidth($value, 'UTF-8');
$len_stripped = mb_strwidth(strip_tags($value), 'UTF-8');
$len_tags = $len - $len_stripped;
$value = mb_strimwidth($value, 0, $limit + $len_tags, '', 'UTF-8');
} while ($len_stripped > $limit);
// Load as HTML ignoring errors
$dom = new DOMDocument();
#$dom->loadHTML('<?xml encoding="utf-8" ?>'.$value, LIBXML_HTML_NODEFDTD);
// Fix the html errors
$value = $dom->saveHtml($dom->getElementsByTagName('body')->item(0));
// Remove body tag
$value = mb_strimwidth($value, 6, mb_strwidth($value, 'UTF-8') - 13, '', 'UTF-8'); // <body> and </body>
// Remove empty tags
return preg_replace('/<(\w+)\b(?:\s+[\w\-.:]+(?:\s*=\s*(?:"[^"]*"|"[^"]*"|[\w\-.:]+))?)*\s*\/?>\s*<\/\1\s*>/', '', $value);
}
SEE DEMO.
I recommend use html_entity_decode at the start of function, so it preserves the UTF-8 characters:
$value = html_entity_decode($value);
Use a HTML parser and stop after 100 characters of text.
You should use Tidy HTML. You cut the string and then you run Tidy to close the tags.
(Credits where credits are due)
Regardless of the 100 count issues you state at the beginning, you indicate in the challenge the following:
output the character count of
strip_tags (the number of characters
in the actual displayed text of the
HTML)
retain HTML formatting close
any unfinished HTML tag
Here is my proposal:
Bascially, I parse through each character counting as I go. I make sure NOT to count any characters in any HTML tag. I also check at the end to make sure I am not in the middle of a word when I stop. Once I stop, I back track to the first available SPACE or > as a stopping point.
$position = 0;
$length = strlen($content)-1;
// process the content putting each 100 character section into an array
while($position < $length)
{
$next_position = get_position($content, $position, 100);
$data[] = substr($content, $position, $next_position);
$position = $next_position;
}
// show the array
print_r($data);
function get_position($content, $position, $chars = 100)
{
$count = 0;
// count to 100 characters skipping over all of the HTML
while($count <> $chars){
$char = substr($content, $position, 1);
if($char == '<'){
do{
$position++;
$char = substr($content, $position, 1);
} while($char !== '>');
$position++;
$char = substr($content, $position, 1);
}
$count++;
$position++;
}
echo $count."\n";
// find out where there is a logical break before 100 characters
$data = substr($content, 0, $position);
$space = strrpos($data, " ");
$tag = strrpos($data, ">");
// return the position of the logical break
if($space > $tag)
{
return $space;
} else {
return $tag;
}
}
This will also count the return codes etc. Considering they will take space, I have not removed them.
Here is a function I'm using in one of my projects. It's based on DOMDocument, works with HTML5 and is about 2x faster than other solutions I've tried (at least on my machine, 0.22 ms vs 0.43 ms using html_cut($text, $max_length) from the top answer on a 500 text-node-characters string with a limit of 400).
function cut_html ($html, $limit) {
$dom = new DOMDocument();
$dom->loadHTML(mb_convert_encoding("<div>{$html}</div>", "HTML-ENTITIES", "UTF-8"), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
cut_html_recursive($dom->documentElement, $limit);
return substr($dom->saveHTML($dom->documentElement), 5, -6);
}
function cut_html_recursive ($element, $limit) {
if($limit > 0) {
if($element->nodeType == 3) {
$limit -= strlen($element->nodeValue);
if($limit < 0) {
$element->nodeValue = substr($element->nodeValue, 0, strlen($element->nodeValue) + $limit);
}
}
else {
for($i = 0; $i < $element->childNodes->length; $i++) {
if($limit > 0) {
$limit = cut_html_recursive($element->childNodes->item($i), $limit);
}
else {
$element->removeChild($element->childNodes->item($i));
$i--;
}
}
}
}
return $limit;
}
Here is my try at the cutter. Maybe you guys can catch some bugs. The problem, i found with the other parsers, is that they don't close tags properly and they cut in the middle of a word (blah)
function cutHTML($string, $length, $patternsReplace = false) {
$i = 0;
$count = 0;
$isParagraphCut = false;
$htmlOpen = false;
$openTag = false;
$tagsStack = array();
while ($i < strlen($string)) {
$char = substr($string, $i, 1);
if ($count >= $length) {
$isParagraphCut = true;
break;
}
if ($htmlOpen) {
if ($char === ">") {
$htmlOpen = false;
}
} else {
if ($char === "<") {
$j = $i;
$char = substr($string, $j, 1);
while ($j < strlen($string)) {
if($char === '/'){
$i++;
break;
}
elseif ($char === ' ') {
$tagsStack[] = substr($string, $i, $j);
}
$j++;
}
$htmlOpen = true;
}
}
if (!$htmlOpen && $char != ">") {
$count++;
}
$i++;
}
if ($isParagraphCut) {
$j = $i;
while ($j > 0) {
$char = substr($string, $j, 1);
if ($char === " " || $char === ";" || $char === "." || $char === "," || $char === "<" || $char === "(" || $char === "[") {
break;
} else if ($char === ">") {
$j++;
break;
}
$j--;
}
$string = substr($string, 0, $j);
foreach($tagsStack as $tag){
$tag = strtolower($tag);
if($tag !== "img" && $tag !== "br"){
$string .= "</$tag>";
}
}
$string .= "...";
}
if ($patternsReplace) {
foreach ($patternsReplace as $value) {
if (isset($value['pattern']) && isset($value["replace"])) {
$string = preg_replace($value["pattern"], $value["replace"], $string);
}
}
}
return $string;
}
try this function
// trim the string function
function trim_word($text, $length, $startPoint=0, $allowedTags=""){
$text = html_entity_decode(htmlspecialchars_decode($text));
$text = strip_tags($text, $allowedTags);
return $text = substr($text, $startPoint, $length);
}
and
echo trim_word("<h2 class='zzzz'>abcasdsdasasdas</h2>","6");
I know this is quite old, but I've recently made a small class for cutting HTML for previews: https://github.com/Simbiat/HTMLCut/
Why would you want to use that instead of the other suggestions? Here are a few things that come to my mind (taken from readme):
Preserve HTML tags, unless they are empty.
Preserve words.
Remove some orphaned punctuation signs at the end of the cut string.
Remove HTML tags, that you would not want in a preview (optional).
Limit number of paragraphs (optional).
Add an ellipsis if text was cut (optional).
Class operates with DOM, but also uses Regex in some places (mainly for cutting and trimming). Perhaps it can be of use to some.
Try the following:
<?php echo strip_tags(mb_strimwidth($VARIABLE_HERE, 0, 160, "...")); ?>
This will strip the HTML (strip_tags) amd limit characters (mb_strimwidth) to 160 characters

How to trim all leading/trailing <br> code using php

I am trying to remove all leading and trailing <br> in a string using PHP.
Here is an example
<br><br>
Hello<br>
World<br>
<p>This is a message<br>...</p>
<br><br><br><br>
I want to return
Hello<br>
World<br>
<p>This is a message<br>...</p>
I tried to do the following
echo trim($str, '<br>');
But it does not remove them. How can I remove the new line html code?
Use preg_replace with the beginning ^ and end $ anchors:
$string = preg_replace('/^(<br>){0,}|(<br>){0,}$/', '', $string);
Or for multiple lines:
$string = preg_replace('/^(<br>){0,}|(<br>){0,}$/m', '', $string);
You could also trim() it multiple times:
while($string !== ($string = trim($string, '<br>'))){}
This function does the job. Also applicable to anything else really.
//remove all leading and trailing occurences of needle ($n) from haystack ($h)
function trimAll($h, $n){
if(!$h = trim($h,$n)){
trimAll($h, $n);
}
return $h;
}
I wrote this function that will do the job a little better as it gives me more flexibility on what characters to remove and when this function by default will first remove the leading/trailing characters in order:
any tabs
any new lines
any
any
any tabs
any new lines
function trimString($str, $myList = array("\t","\n", "<br>","<br />", "\t","\n") ){
if( ! is_array($myList) ){
$charsToTrim[] = $chr;
} else {
$charsToTrim = $myList;
}
foreach($charsToTrim as $chr){
$len = strlen($chr);
$nlen = $len * -1;
while( substr($str, 0, $len) == $chr){
$str = trim(substr($str, $len));
}
while( substr($str, $nlen) == $chr){
$str = trim(substr($str, 0, $nlen));
}
}
return $str;
}
to use
// default use case
echo trimString($message);
or
//remove only one string
echo trimString($message, '<br>'); // remove only the leading training '<br>'
or
//remove more than 1 string in order
echo trimString($message, array('<br>'<br />') );
I hope this helps someone out there :)
$p=array(
'<br><br>',
'Hello<br>',
'World<br>',
'<p>This is a message<br>...</p>',
'<br><br><br><br>'
);
function trimdeluxe($str, $sub)
{
$parts=explode($sub, $str);
for ($x=0; $x<2; $x++) {
foreach ($parts as $i=>$v) {
if (!strlen($v)) {
unset($parts[$i]);
} else {
break;
}
}
$parts=array_reverse($parts);
}
return implode($sub,$parts);
}
foreach ($p as $str) {
print $str . ' -> ' . trimdeluxe($str, '<br>') . "\n";
}

Parse HTML user input

Let's say I have a string from the user ($input). I can go and strip tags, to allow only allowed tags in. I can convert to text with htmlspecialchars(). I can even replace all tags I don't want with text.
function html($input) {
$input = '<bl>'.htmlspecialchars($input).'</bl>'; // bl is a custom tag that I style (stands for block)
global $open;
$open = []; //Array of open tags
for ($i = 0; $i < strlen($input); $i++) {
if (!in_array('code', $open) && !in_array('codebl', $open)) { //If we are parsing
$input = preg_replace_callback('#^(.{'.$i.'})<(em|i|del|sub|sup|sml|code|kbd|pre|codebl|quote|bl|sbl)>\s*#s', function($match) {
global $open; //...then add new tags to the array
array_push($open,$match[2]);
return $match[1].'<'.$match[2].'>'; //And replace them
}, $input);
$input = preg_replace_callback('#^(.{'.$i.'})(https?):\/\/([^\s"\(\)<>]+)#', function($m) {
return $m[1].''.$m[3].'';
}, $input, -1, $num); //Simple linking
$i += $num * 9;
$input = preg_replace_callback('#^(.{'.$i.'})\n\n#', function($m) {
return $m[1].'</bl><bl>';
}, $input); // More of this bl element
}
if (end($open)) { //Close tags
$input = preg_replace_callback('#^(.{'.$i.'})</('.end($open).')>#s', function($match) {
global $open;
array_pop($open);
return trim($match[1]).'</'.$match[2].'>';
}, $input);
}
}
while ($open) { //Handle unclosed tags
$input .= '</'.end($open).'>';
array_pop($open);
}
return $input;
}
The problem is that after that, there is no way to write literally <i&lgt;</i>, because it will be automatically parsed into either <i></i> (if you write <i></i>), or &amplt;i&ampgt;&amplt;/i&ampgt; (if you write <i></i>). I want the user to be able to enter < (or any other HTML entity) and get < back. If I just send it straight to the browser unparsed, it would (obviously) be vulnerable to whatever sorcery the hackers are trying (and I'm letting) to (be) put on my site. So, How can I let the user use any of the pre-defined set of HTML tags, while still letting them use html entities?
This is what I eventually used:
function html($input) {
$input = preg_replace(["#&([^A-z])#","#<([^A-z/])#","#&$#","#<$#"], ['&$1','<$1','&','<'], $input); //Fix single "<"s and "&"s
$open = []; //Array of open tags
$close = false; //Is the current tag a close tag?
for ($i = 0; $i <= strlen($input); $i++) { //Start the loop
if ($tag) { //Are we in a tag?
if (preg_match("/[^a-z]/", $input[$i])) { //The tag has ended
if ($close) {
$close = false;
$sPos = strrpos(substr($input,0,$i), '<') + 2; //start position of tag
$tag = substr($input,$sPos,$i-$sPos); //tag name
if (end($open) == $tag) {
array_pop($open); //Good, it's a valid XML closing
} else {
$input = substr($input, 0, $sPos-2) . '</' . $tag . substr($input, $i); //BAD! Convert tag to text (open tag will be handled later)
}
} else {
$sPos = strrpos(substr($input,0,$i), '<') + 1; //start position of tag
$tag = substr($input,$sPos,$i-$sPos); //tag name
if (in_array($tag, ['em','i','del','sub','sup','sml','code','kbd','pre','codebl','bl','sbl'])) { //Is it an acceptable tag?
array_push($open, $tag); //Add it to the array
$j = $i + 1;
while (preg_match("/\s/", $input[$j])) { //Get rid of whitespace
$j++;
}
$input = substr($input, 0, $sPos - 1) . '<' . $tag . '>' . substr($input, $j); //Seems legit
} else {
$input = substr($input, 0, $sPos - 1) . '<' . $tag . substr($input, $i); //BAD! Convert tag to text
}
}
$tag = false;
}
} else if (!in_array('code', $open) && !in_array('codebl', $open) && !in_array('pre', $open)) { //Standard parsing of text
if ($input[$i] == '<') { //Is it a tag?
$tag = true;
if ($input[$i+1] == '/') { //Is it a close tag?
$i++;
$close = true;
}
} else if (substr($input, $i, 4) == 'http') { //Link
if (preg_match('#^.{'.$i.'}(https?):\/\/([^\s"\(\)<>]+)#', $input, $m)) {
$insert = ''.$m[2].'';
$input = substr($input, 0, $i) . $insert . substr($input, $i + strlen($m[1].'://'.$m[2]));
$i += strlen($insert);
}
} else if ($input[$i] == "\n" && $input[$i+1] == "\n") { //Insert <bl> tag? (I use this to separate sections of text)
$input = substr($input, 0, $i + 1) . '</bl><bl>' . substr($input, $i + 1);
}
} else { // We're in a code tag
if (substr($input, $i+1, strlen(end($open)) + 3) == '</'.current($open).'>') {
array_pop($open);
$i += 2;
} elseif ($input[$i] == '<') {
$input = substr($input, 0, $i) . '<' . substr($input, $i + 1);
$i += 3; //Code tags have raw text
} elseif (in_array('code', $open) && $input[$i] == "\n") { //No linebreaks are allowed in inline tags, convert to <codebl>
$open[count($open) - 1] = 'codebl';
$input = substr($input, 0, strrpos($input,'<code>')) . '<codebl>' . substr($input, strrpos($input,'<code>') + 6, strpos(substr($input, strrpos($input,'<code>')),'</code>') - 6) . '</codebl>' . substr($input, strpos(substr($input, strrpos($input,'<code>')),'</code>') + strrpos($input,'<code>') + 7);
$i += 4;
}
}
}
while ($open) { //Handle open tags
$input .= '</'.end($open).'>';
array_pop($open);
}
return '<bl>'.$input.'</bl>';
}
I know it's a bit more risky, but you can first assume the input's good, then filter out the stuff explicitly found as bad.

Shortening text tweet-like without cutting links inside

I've got a string like this:
I love #kevinrose 's new website Link
And I have this function:
function short($string, $max = 255) {
if (strlen($string) >= $max) {
$string = mb_substr($string, 0, $max - 5, 'utf-8') . '...';
} return $string;
}
If I cut the screen at 50, it will end up being something that :
I love #kevinrose 's new website <a href="http://kevinr...
Which will of course kill the html.
Is there an easy way I can avoid cutting the a href tag (before or preferably after) without breaking the HTML ?
I need to keep my tags of course.
Thank you
see this from PHP: Truncate string while preserving HTML tags and whole words - Alan Whipple -> http://alanwhipple.com/2011/05/25/php-truncate-string-preserving-html-tags-words/
<?php
/**
* truncateHtml can truncate a string up to a number of characters while preserving whole words and HTML tags
*
* #param string $text String to truncate.
* #param integer $length Length of returned string, including ellipsis.
* #param string $ending Ending to be appended to the trimmed string.
* #param boolean $exact If false, $text will not be cut mid-word
* #param boolean $considerHtml If true, HTML tags would be handled correctly
*
* #return string Trimmed string.
*/
function truncateHtml($text, $length = 100, $ending = '...', $exact = false, $considerHtml = true) {
if ($considerHtml) {
// if the plain text is shorter than the maximum length, return the whole text
if (strlen(preg_replace('/<.*?>/', '', $text)) <= $length) {
return $text;
}
// splits all html-tags to scanable lines
preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
$total_length = strlen($ending);
$open_tags = array();
$truncate = '';
foreach ($lines as $line_matchings) {
// if there is any html-tag in this line, handle it and add it (uncounted) to the output
if (!empty($line_matchings[1])) {
// if it's an "empty element" with or without xhtml-conform closing slash
if (preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $line_matchings[1])) {
// do nothing
// if tag is a closing tag
} else if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $line_matchings[1], $tag_matchings)) {
// delete tag from $open_tags list
$pos = array_search($tag_matchings[1], $open_tags);
if ($pos !== false) {
unset($open_tags[$pos]);
}
// if tag is an opening tag
} else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $line_matchings[1], $tag_matchings)) {
// add tag to the beginning of $open_tags list
array_unshift($open_tags, strtolower($tag_matchings[1]));
}
// add html-tag to $truncate'd text
$truncate .= $line_matchings[1];
}
// calculate the length of the plain text part of the line; handle entities as one character
$content_length = strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|[0-9a-f]{1,6};/i', ' ', $line_matchings[2]));
if ($total_length+$content_length> $length) {
// the number of characters which are left
$left = $length - $total_length;
$entities_length = 0;
// search for html entities
if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|[0-9a-f]{1,6};/i', $line_matchings[2], $entities, PREG_OFFSET_CAPTURE)) {
// calculate the real length of all entities in the legal range
foreach ($entities[0] as $entity) {
if ($entity[1]+1-$entities_length <= $left) {
$left--;
$entities_length += strlen($entity[0]);
} else {
// no more characters left
break;
}
}
}
$truncate .= substr($line_matchings[2], 0, $left+$entities_length);
// maximum lenght is reached, so get off the loop
break;
} else {
$truncate .= $line_matchings[2];
$total_length += $content_length;
}
// if the maximum length is reached, get off the loop
if($total_length>= $length) {
break;
}
}
} else {
if (strlen($text) <= $length) {
return $text;
} else {
$truncate = substr($text, 0, $length - strlen($ending));
}
}
// if the words shouldn't be cut in the middle...
if (!$exact) {
// ...search the last occurance of a space...
$spacepos = strrpos($truncate, ' ');
if (isset($spacepos)) {
// ...and cut the text in this position
$truncate = substr($truncate, 0, $spacepos);
}
}
// add the defined ending to the text
$truncate .= $ending;
if($considerHtml) {
// close all unclosed html-tags
foreach ($open_tags as $tag) {
$truncate .= '</' . $tag . '>';
}
}
return $truncate;
}
?>
also see here
Truncating Text & HTML with PHP | Patrick Galbraith ->
http://www.pjgalbraith.com/2011/11/truncating-text-html-with-php/
Here's a bit shorter method. It doesn't walk the DOM tree, but it will work for nearly all situations.
This method first strips all html tags from the content (so html tags won't be counted towards the string length either). Then, if the string needs to be truncated, it truncates it and re-inserts all html tags.
<?php
function short($string, $max = 255) {
preg_match_all('/<[^>]+>/', $string, $tags); // Save tag information for later
$stripped = preg_replace('/<[^>]+>/', '', $string); // Strip html tags
// Truncate the string if needed
if (strlen($stripped) > $max) {
$truncated = mb_substr($stripped, 0, $max, 'utf-8');
// Insert html tags, if any
if (sizeof($tags) > 0) {
$pos = 0;
foreach ($tags[0] as $tag) {
$pos += strpos($string, $tag); // Get the position the tag should be inserted at
$string = substr($string, $pos); // Shift to avoid issues with duplicate tags
$truncated = substr_replace($truncated, $tag, $pos, 0); // Insert the tag
}
}
$string = $truncated . '…';
}
return $string;
}
echo short('I love #kevinrose\'s new website Link. Here is a bit of additional text after the link.<a></a>', 50);

how to validate the number of opened and closed tags?

I thought to do a preg_count for each "/<[a-z0-9]+>/i" and then count if exists the same number with the closed tags ie: "/</[a-z0-9]+>/i"
But I am not too sure. How would you count all opened tags and check if exists all closed tags?
Ps. i don't need to check for attribute and for xml /> single close tag. I just need a count on plain simple html tag
Thanks
I wrote this handy functions. I think it could be faster if I search both opened/closed tags within one preg_match_all but as this it's more readable:
<?php
//> Will count number of <[a-z]> tag and </[a-z]> tag (will also validate the order)
//> Note br should be in the form of <br /> for not causing problems
function validHTML($html,$checkOrder=true) {
preg_match_all( '#<([a-z]+)>#i' , $html, $start, PREG_OFFSET_CAPTURE );
preg_match_all( '#<\/([a-z]+)>#i' , $html, $end, PREG_OFFSET_CAPTURE );
$start = $start[1];
$end = $end[1];
if (count($start) != count($end) )
throw new Exception('Check numbers of tags');
if ($checkOrder) {
$is = 0;
foreach($end as $v){
if ($v[0] != $start[$is][0] || $v[1] < $start[$is][1] )
throw new Exception('End tag ['.$v[0].'] not opened');
$is++;
}
}
return true;
}
//> Usage::
try {
validHTML('<p>hello</p><li></li></p><p>');
} catch (Exception $e) {
echo $e->getMessage();
}
Note if you need to catch even h1 or any other tag with numbers you need to add 0-9 within pattern of preg
The proper way to validate HTML is using a HTML parser. Using Regexes to deal with HTML is very wrong - see RegEx match open tags except XHTML self-contained tags
My case
function checkHtml($html) {
$level = 0;
$map = [];
$length = strlen($html);
$open = false;
$tag = '';
for($i = 0; $i < $length; $i ++) {
$c = substr($html, $i, 1);
if($c == '<') {
$open = true;
$tag = '';
} else if($open && ($c == '>' || ord($c) == 32)) {
$open = false;
if(in_array($tag, ['br', 'br/', 'hr/', 'img/', 'hr', 'img'])) {
continue;
}
if(strpos($tag, '/') === 0) {
if(!isset($map[$tag.($level-1)])) {
return false;
}
$level --;
unset($map[$tag.$level]);
} else {
$map['/'.$tag.$level] = true;
$level ++;
}
} else if($open) {
$tag .= $c;
}
}
return $level == 0;
}
ok, one solution would be:
function open_tags($page)
{
$arr=array();
$page // your html/xml/somthing content
$i=0;
while ($i<strlen($page))
{
$i=strpos($page,'<',$i); //position of starting the tag
$end=strpos($page,'>',$i); //position of ending the tag
if(strpos($page,'/')<$end) //if it's an end tag
{
if (array_pop($arr)!=substr($page,$i,$end-$i)); // pop the last value inserted into the stack, and check if it's the same as this one
return FALSE;
}
else
{
array_push($arr,substr($page,$i,$end-$i)); // push the new tag value into the stack
}
}
return $arr;
}
this will return opened tags by order, or false if error.
edit:
function open_tags($page)
{
$arr=array();
$page // your html/xml/somthing content
$i=0;
while ($i<strlen($page))
{
$i=strpos($page,'<',$i); //position of starting the tag
$end=strpos($page,'>',$i); //position of ending the tag
if($end>strpos($page,'<',$i))
return false;
if(strpos($page,'/')<$end) //if it's an end tag
{
if (array_pop($arr)!=substr($page,$i,$end-$i)); // pop the last value inserted into the stack, and check if it's the same as this one
return FALSE;
}
else
{
array_push($arr,substr($page,$i,$end-$i)); // push the new tag value into the stack
}
}
return $arr;
}

Categories