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 // Nonbreaking spaces get converted into weird characters, so strip them
43 // empty patragraphs too
44 $value = trim(preg_replace('!<p>\s*</p>!is', '', str_replace(array("\xC2\xA0", ' '), ' ', $this->value)));
45
46 /* First we need the text of the first paragraph, without tags. Try using SimpleXML first */
47 if (class_exists('SimpleXMLElement')) {
48 $doc = new DOMDocument();
49
50 /* Catch warnings thrown by loadHTML and turn them into a failure boolean rather than a SilverStripe error */
51 set_error_handler(create_function('$no, $str', 'throw new Exception("HTML Parse Error: ".$str);'), E_ALL);
52 try { $res = $doc->loadHTML('<meta content="text/html; charset=utf-8" http-equiv="Content-type"/>' . $value); }
53 catch (Exception $e) { $res = false; }
54 restore_error_handler();
55
56 if ($res) {
57 $xml = simplexml_import_dom($doc);
58 $res = $xml->xpath('//p');
59 if (!empty($res)) $str = strip_tags($res[0]->asXML());
60 }
61 }
62
63 /* 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
64 * that does very badly on broken HTML*/
65 if (!$str) {
66 /* See if we can pull a paragraph out*/
67 if (preg_match('{<p(\s[^<>]*)?>(.*[A-Za-z]+.*)</p>}', $value, $matches)) $str = $matches[2];
68 /* If _that_ failed, just use the whole text */
69 else $str = $value;
70
71 /* Now pull out all the html-alike stuff */
72 $str = preg_replace('{</?[a-zA-Z]+[^<>]*>}', '', $str); /* Take out anything that is obviously a tag */
73 $str = preg_replace('{</|<|>}', '', $str); /* Strip out any left over looking bits. Textual < or > should already be encoded to < or > */
74 }
75
76 /* Now split into words. If we are under the maxWords limit, just return the whole string (re-implode for whitespace normalization) */
77 $words = preg_split('/\s+/', $str);
78 if ($maxWords == -1 || count($words) <= $maxWords) return implode(' ', $words);
79
80 /* Otherwise work backwards for a looking for a sentence ending (we try to avoid abbreviations, but aren't very good at it) */
81 for ($i = $maxWords; $i >= $maxWords - $flex && $i >= 0; $i--) {
82 if (preg_match('/\.$/', $words[$i]) && !preg_match('/(Dr|Mr|Mrs|Ms|Miss|Sr|Jr|No)\.$/i', $words[$i])) {
83 return implode(' ', array_slice($words, 0, $i+1));
84 }
85 }
86
87 /* If we didn't find a sentence ending quickly enough, just cut at the maxWords point and add '...' to the end */
88 return implode(' ', array_slice($words, 0, $maxWords)) . $add;
89 }
90
91 /**
92 * Returns the first sentence from the first paragraph. If it can't figure out what the first paragraph is (or there isn't one)
93 * it returns the same as Summary()
94 *
95 * This is the HTML aware equivilent to Text#FirstSentence
96 *
97 * @see sapphire/core/model/fieldtypes/Text#FirstSentence()
98 */
99 function FirstSentence() {
100 /* Use summary's html processing logic to get the first paragraph */
101 $paragraph = $this->Summary(-1);
102
103 /* Then look for the first sentence ending. We could probably use a nice regex, but for now this will do */
104 $words = preg_split('/\s+/', $paragraph);
105 foreach ($words as $i => $word) {
106 if (preg_match('/\.$/', $word) && !preg_match('/(Dr|Mr|Mrs|Ms|Miss|Sr|Jr|No)\.$/i', $word)) {
107 return implode(' ', array_slice($words, 0, $i+1));
108 }
109 }
110
111 /* 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 */
112 return $this->Summary();
113 }
114
115 public function forTemplate() {
116 return ShortcodeParser::get_active()->parse($this->value);
117 }
118
119 public function hasValue() {
120 return parent::hasValue() && $this->value != '<p></p>';
121 }
122
123 public function scaffoldFormField($title = null, $params = null) {
124 return new HtmlEditorField($this->name, $title);
125 }
126
127 public function scaffoldSearchField($title = null) {
128 return new TextField($this->name, $title);
129 }
130
131 }
132
133 ?>