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(' ', ' ', $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 < or > */
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 ?>