Version: current

      1 <?php
      2 /**
      3  * Represents a large text field that contains HTML content.
      4  *
      5  * This behaves similarly to Text, but the template processor won't escape any HTML content within it.
      6  * @package sapphire
      7  * @subpackage model
      8  */
      9 class HTMLText extends Text {
     10
     11     public static $escape_type = 'xml';
     12
     13     /**
     14      * Limit this field's content by a number of characters.
     15      * This makes use of strip_tags() to avoid malforming the
     16      * HTML tags in the string of text.
     17      *
     18      * @param int $limit Number of characters to limit by
     19      * @param string $add Ellipsis to add to the end of truncated string
     20      * @return string
     21      */
     22     function LimitCharacters($limit = 20, $add = "...") {
     23         $value = trim(strip_tags($this->value));
     24         return (mb_strlen($value) > $limit) ? mb_substr($value, 0, $limit) . $add : $value;
     25     }
     26
     27     /**
     28      * Create a summary of the content. This will be some section of the first paragraph, limited by
     29      * $maxWords. All internal tags are stripped out - the return value is a string
     30      *
     31      * This is sort of the HTML aware equivilent to Text#Summary, although the logic for summarising is not exactly the same
     32      *
     33      * @param int $maxWords Maximum number of words to return - may return less, but never more. Pass -1 for no limit
     34      * @param int $flex Number of words to search through when looking for a nice cut point
     35      * @param string $add What to add to the end of the summary if we cut at a less-than-ideal cut point
     36      * @return string A nice(ish) summary with no html tags (but possibly still some html entities)
     37      *
     38      * @see sapphire/core/model/fieldtypes/Text#Summary($maxWords)
     39      */
     40     public function Summary($maxWords = 50, $flex = 15, $add = '...') {
     41         $str = false;
     42
     43         /* First we need the text of the first paragraph, without tags. Try using SimpleXML first */
     44         if (class_exists('SimpleXMLElement')) {
     45             $doc = new DOMDocument();
     46
     47             /* Catch warnings thrown by loadHTML and turn them into a failure boolean rather than a SilverStripe error */
     48             set_error_handler(create_function('$no, $str', 'throw new Exception("HTML Parse Error: ".$str);'), E_ALL);
     49             //  Nonbreaking spaces get converted into weird characters, so strip them
     50             $value = str_replace('&nbsp;', ' ', $this->value);
     51             try { $res = $doc->loadHTML('<meta content="text/html; charset=utf-8" http-equiv="Content-type"/>' . $value); }
     52             catch (Exception $e) { $res = false; }
     53             restore_error_handler();
     54
     55             if ($res) {
     56                 $xml = simplexml_import_dom($doc);
     57                 $res = $xml->xpath('//p');
     58                 if (!empty($res)) $str = strip_tags($res[0]->asXML());
     59             }
     60         }
     61
     62         /* If that failed, most likely the passed HTML is broken. use a simple regex + a custom more brutal strip_tags. We don't use strip_tags because
     63          * that does very badly on broken HTML*/
     64         if (!$str) {
     65             /* See if we can pull a paragraph out*/
     66             if (preg_match('{<p(\s[^<>]*)?>(.*[A-Za-z]+.*)</p>}', $this->value, $matches)) $str = $matches[2];
     67             /* If _that_ failed, just use the whole text */
     68             else $str = $this->value;
     69
     70             /* Now pull out all the html-alike stuff */
     71             $str = preg_replace('{</?[a-zA-Z]+[^<>]*>}', '', $str); /* Take out anything that is obviously a tag */
     72             $str = preg_replace('{</|<|>}', '', $str); /* Strip out any left over looking bits. Textual < or > should already be encoded to &lt; or &gt; */
     73         }
     74
     75         /* Now split into words. If we are under the maxWords limit, just return the whole string (re-implode for whitespace normalization) */
     76         $words = preg_split('/\s+/', $str);
     77         if ($maxWords == -1 || count($words) <= $maxWords) return implode(' ', $words);
     78
     79         /* Otherwise work backwards for a looking for a sentence ending (we try to avoid abbreviations, but aren't very good at it) */
     80         for ($i = $maxWords; $i >= $maxWords - $flex && $i >= 0; $i--) {
     81             if (preg_match('/\.$/', $words[$i]) && !preg_match('/(Dr|Mr|Mrs|Ms|Miss|Sr|Jr|No)\.$/i', $words[$i])) {
     82                 return implode(' ', array_slice($words, 0, $i+1));
     83             }
     84         }
     85
     86         /* If we didn't find a sentence ending quickly enough, just cut at the maxWords point and add '...' to the end */
     87         return implode(' ', array_slice($words, 0, $maxWords)) . $add;
     88     }
     89
     90     /**
     91      * Returns the first sentence from the first paragraph. If it can't figure out what the first paragraph is (or there isn't one)
     92      * it returns the same as Summary()
     93      *
     94      * This is the HTML aware equivilent to Text#FirstSentence
     95      *
     96      * @see sapphire/core/model/fieldtypes/Text#FirstSentence()
     97      */
     98     function FirstSentence() {
     99         /* Use summary's html processing logic to get the first paragraph */
    100         $paragraph = $this->Summary(-1);
    101
    102         /* Then look for the first sentence ending. We could probably use a nice regex, but for now this will do */
    103         $words = preg_split('/\s+/', $paragraph);
    104         foreach ($words as $i => $word) {
    105             if (preg_match('/\.$/', $word) && !preg_match('/(Dr|Mr|Mrs|Ms|Miss|Sr|Jr|No)\.$/i', $word)) {
    106                 return implode(' ', array_slice($words, 0, $i+1));
    107             }
    108         }
    109
    110         /* If we didn't find a sentence ending, use the summary. We re-call rather than using paragraph so that Summary will limit the result this time */
    111         return $this->Summary();
    112     }
    113
    114     public function forTemplate() {
    115         return ShortcodeParser::get_active()->parse($this->value);
    116     }
    117
    118     public function hasValue() {
    119         return parent::hasValue() && $this->value != '<p></p>';
    120     }
    121
    122     public function scaffoldFormField($title = null, $params = null) {
    123         return new HtmlEditorField($this->name, $title);
    124     }
    125
    126     public function scaffoldSearchField($title = null) {
    127         return new TextField($this->name, $title);
    128     }
    129
    130 }
    131
    132 ?>

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -