4 * Copyright (c) 2005-2007 Jon Abernathy <jon@chuggnutt.com>
6 * This script is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * The GNU General Public License can be found at
12 * http://www.gnu.org/copyleft/gpl.html.
14 * This script is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
24 const ENCODING
= 'UTF-8';
26 protected $htmlFuncFlags;
29 * Contains the HTML content to convert.
36 * Contains the converted, formatted text.
43 * List of preg* regular expression patterns to search for,
44 * used in conjunction with $replace.
49 protected $search = array(
50 "/\r/", // Non-legal carriage return
51 "/[\n\t]+/", // Newlines and tabs
52 '/<head\b[^>]*>.*?<\/head>/i', // <head>
53 '/<script\b[^>]*>.*?<\/script>/i', // <script>s -- which strip_tags supposedly has problems with
54 '/<style\b[^>]*>.*?<\/style>/i', // <style>s -- which strip_tags supposedly has problems with
55 '/<i\b[^>]*>(.*?)<\/i>/i', // <i>
56 '/<em\b[^>]*>(.*?)<\/em>/i', // <em>
57 '/(<ul\b[^>]*>|<\/ul>)/i', // <ul> and </ul>
58 '/(<ol\b[^>]*>|<\/ol>)/i', // <ol> and </ol>
59 '/(<dl\b[^>]*>|<\/dl>)/i', // <dl> and </dl>
60 '/<li\b[^>]*>(.*?)<\/li>/i', // <li> and </li>
61 '/<dd\b[^>]*>(.*?)<\/dd>/i', // <dd> and </dd>
62 '/<dt\b[^>]*>(.*?)<\/dt>/i', // <dt> and </dt>
63 '/<li\b[^>]*>/i', // <li>
64 '/<hr\b[^>]*>/i', // <hr>
65 '/<div\b[^>]*>/i', // <div>
66 '/(<table\b[^>]*>|<\/table>)/i', // <table> and </table>
67 '/(<tr\b[^>]*>|<\/tr>)/i', // <tr> and </tr>
68 '/<td\b[^>]*>(.*?)<\/td>/i', // <td> and </td>
69 '/<span class="_html2text_ignore">.+?<\/span>/i', // <span class="_html2text_ignore">...</span>
70 '/<(img)\b[^>]*alt=\"([^>"]+)\"[^>]*>/i', // <img> with alt tag
74 * List of pattern replacements corresponding to patterns searched.
79 protected $replace = array(
80 '', // Non-legal carriage return
81 ' ', // Newlines and tabs
83 '', // <script>s -- which strip_tags supposedly has problems with
84 '', // <style>s -- which strip_tags supposedly has problems with
87 "\n\n", // <ul> and </ul>
88 "\n\n", // <ol> and </ol>
89 "\n\n", // <dl> and </dl>
90 "\t* \\1\n", // <li> and </li>
91 " \\1\n", // <dd> and </dd>
92 "\t* \\1", // <dt> and </dt>
94 "\n-------------------------\n", // <hr>
96 "\n\n", // <table> and </table>
97 "\n", // <tr> and </tr>
98 "\t\t\\1\n", // <td> and </td>
99 "", // <span class="_html2text_ignore">...</span>
100 '[\\2]', // <img> with alt tag
104 * List of preg* regular expression patterns to search for,
105 * used in conjunction with $entReplace.
110 protected $entSearch = array(
111 '/™/i', // TM symbol in win-1252
112 '/—/i', // m-dash in win-1252
113 '/&(amp|#38);/i', // Ampersand: see converter()
114 '/[ ]{2,}/', // Runs of spaces, post-handling
118 * List of pattern replacements corresponding to patterns searched.
123 protected $entReplace = array(
126 '|+|amp|+|', // Ampersand: see converter()
127 ' ', // Runs of spaces, post-handling
131 * List of preg* regular expression patterns to search for
132 * and replace using callback function.
136 protected $callbackSearch = array(
137 '/<(h)[123456]( [^>]*)?>(.*?)<\/h[123456]>/i', // h1 - h6
138 '/[ ]*<(p)( [^>]*)?>(.*?)<\/p>[ ]*/si', // <p> with surrounding whitespace.
139 '/<(br)[^>]*>[ ]*/i', // <br> with leading whitespace after the newline.
140 '/<(b)( [^>]*)?>(.*?)<\/b>/i', // <b>
141 '/<(strong)( [^>]*)?>(.*?)<\/strong>/i', // <strong>
142 '/<(th)( [^>]*)?>(.*?)<\/th>/i', // <th> and </th>
143 '/<(a) [^>]*href=("|\')([^"\']+)\2([^>]*)>(.*?)<\/a>/i' // <a href="">
147 * List of preg* regular expression patterns to search for in PRE body,
148 * used in conjunction with $preReplace.
153 protected $preSearch = array(
162 * List of pattern replacements corresponding to patterns searched for PRE body.
167 protected $preReplace = array(
169 ' ',
176 * Temporary workspace used during PRE processing.
180 protected $preContent = '';
183 * Contains the base URL that relative links should resolve to.
187 protected $baseurl = '';
190 * Indicates whether content in the $html variable has been converted yet.
195 protected $converted = false;
198 * Contains URL addresses from links to be rendered in plain text.
201 * @see buildlinkList()
203 protected $linkList = array();
206 * Various configuration options (able to be set in the constructor)
210 protected $options = array(
211 'do_links' => 'inline', // 'none'
212 // 'inline' (show links inline)
213 // 'nextline' (show links on the next line)
214 // 'table' (if a table of link URLs should be listed after the text.
215 // 'bbcode' (show links as bbcode)
217 'width' => 70, // Maximum width of the formatted text, in columns.
218 // Set this value to 0 (or less) to ignore word wrapping
219 // and not constrain text to a fixed-width column.
222 private function legacyConstruct($html = '', $fromFile = false, array $options = array())
224 $this->set_html($html, $fromFile);
225 $this->options
= array_merge($this->options
, $options);
229 * @param string $html Source HTML
230 * @param array $options Set configuration options
232 public function __construct($html = '', $options = array())
234 // for backwards compatibility
235 if (!is_array($options)) {
236 return call_user_func_array(array($this, 'legacyConstruct'), func_get_args());
240 $this->options
= array_merge($this->options
, $options);
241 $this->htmlFuncFlags
= (PHP_VERSION_ID
< 50400)
243 : ENT_COMPAT | ENT_HTML5
;
247 * Get the source HTML
251 public function getHtml()
257 * Set the source HTML
259 * @param string $html HTML source content
261 public function setHtml($html)
264 $this->converted
= false;
270 public function set_html($html, $from_file = false)
273 throw new \
InvalidArgumentException("Argument from_file no longer supported");
276 return $this->setHtml($html);
280 * Returns the text, converted from HTML.
284 public function getText()
286 if (!$this->converted
) {
296 public function get_text()
298 return $this->getText();
304 public function print_text()
306 print $this->getText();
314 return $this->print_text();
318 * Sets a base URL to handle relative links.
320 * @param string $baseurl
322 public function setBaseUrl($baseurl)
324 $this->baseurl
= $baseurl;
330 public function set_base_url($baseurl)
332 return $this->setBaseUrl($baseurl);
335 protected function convert()
337 $origEncoding = mb_internal_encoding();
338 mb_internal_encoding(self
::ENCODING
);
342 mb_internal_encoding($origEncoding);
345 protected function doConvert()
347 $this->linkList
= array();
349 $text = trim($this->html
);
351 $this->converter($text);
353 if ($this->linkList
) {
354 $text .= "\n\nLinks:\n------\n";
355 foreach ($this->linkList
as $i => $url) {
356 $text .= '[' . ($i +
1) . '] ' . $url . "\n";
362 $this->converted
= true;
365 protected function converter(&$text)
367 $this->convertBlockquotes($text);
368 $this->convertPre($text);
369 $text = preg_replace($this->search
, $this->replace
, $text);
370 $text = preg_replace_callback($this->callbackSearch
, array($this, 'pregCallback'), $text);
371 $text = strip_tags($text);
372 $text = preg_replace($this->entSearch
, $this->entReplace
, $text);
373 $text = html_entity_decode($text, $this->htmlFuncFlags
, self
::ENCODING
);
375 // Remove unknown/unhandled entities (this cannot be done in search-and-replace block)
376 $text = preg_replace('/&([a-zA-Z0-9]{2,6}|#[0-9]{2,4});/', '', $text);
378 // Convert "|+|amp|+|" into "&", need to be done after handling of unknown entities
379 // This properly handles situation of "&quot;" in input string
380 $text = str_replace('|+|amp|+|', '&', $text);
382 // Normalise empty lines
383 $text = preg_replace("/\n\s+\n/", "\n\n", $text);
384 $text = preg_replace("/[\n]{3,}/", "\n\n", $text);
386 // remove leading empty lines (can be produced by eg. P tag on the beginning)
387 $text = ltrim($text, "\n");
389 if ($this->options
['width'] > 0) {
390 $text = wordwrap($text, $this->options
['width']);
395 * Helper function called by preg_replace() on link replacement.
397 * Maintains an internal list of links to be displayed at the end of the
398 * text, with numeric indices to the original point in the text they
399 * appeared. Also makes an effort at identifying and handling absolute
400 * and relative links.
402 * @param string $link URL of the link
403 * @param string $display Part of the text to associate number with
404 * @param null $linkOverride
407 protected function buildlinkList($link, $display, $linkOverride = null)
409 $linkMethod = ($linkOverride) ?
$linkOverride : $this->options
['do_links'];
410 if ($linkMethod == 'none') {
414 // Ignored link types
415 if (preg_match('!^(javascript:|mailto:|#)!i', $link)) {
419 if (preg_match('!^([a-z][a-z0-9.+-]+:)!i', $link)) {
422 $url = $this->baseurl
;
423 if (mb_substr($link, 0, 1) != '/') {
429 if ($linkMethod == 'table') {
430 if (($index = array_search($url, $this->linkList
)) === false) {
431 $index = count($this->linkList
);
432 $this->linkList
[] = $url;
435 return $display . ' [' . ($index +
1) . ']';
436 } elseif ($linkMethod == 'nextline') {
437 if ($url === $display) {
440 return $display . "\n[" . $url . ']';
441 } elseif ($linkMethod == 'bbcode') {
442 return sprintf('[url=%s]%s[/url]', $url, $display);
443 } else { // link_method defaults to inline
444 if ($url === $display) {
447 return $display . ' [' . $url . ']';
451 protected function convertPre(&$text)
453 // get the content of PRE element
454 while (preg_match('/<pre[^>]*>(.*)<\/pre>/ismU', $text, $matches)) {
455 // Replace br tags with newlines to prevent the search-and-replace callback from killing whitespace
456 $this->preContent
= preg_replace('/(<br\b[^>]*>)/i', "\n", $matches[1]);
458 // Run our defined tags search-and-replace with callback
459 $this->preContent
= preg_replace_callback(
460 $this->callbackSearch
,
461 array($this, 'pregCallback'),
465 // convert the content
466 $this->preContent
= sprintf(
467 '<div><br>%s<br></div>',
468 preg_replace($this->preSearch
, $this->preReplace
, $this->preContent
)
471 // replace the content (use callback because content can contain $0 variable)
472 $text = preg_replace_callback(
473 '/<pre[^>]*>.*<\/pre>/ismU',
474 array($this, 'pregPreCallback'),
480 $this->preContent
= '';
485 * Helper function for BLOCKQUOTE body conversion.
487 * @param string $text HTML content
489 protected function convertBlockquotes(&$text)
491 if (preg_match_all('/<\/*blockquote[^>]*>/i', $text, $matches, PREG_OFFSET_CAPTURE
)) {
492 $originalText = $text;
497 foreach ($matches[0] as $m) {
498 $m[1] = mb_strlen(substr($originalText, 0, $m[1]));
499 if ($m[0][0] == '<' && $m[0][1] == '/') {
502 $level = 0; // malformed HTML: go to next blockquote
503 } elseif ($level > 0) {
504 // skip inner blockquote
507 $len = $end - $taglen - $start;
508 // Get blockquote content
509 $body = mb_substr($text, $start +
$taglen - $diff, $len);
512 $pWidth = $this->options
['width'];
513 if ($this->options
['width'] > 0) $this->options
['width'] -= 2;
514 // Convert blockquote content
516 $this->converter($body);
517 // Add citation markers and create PRE block
518 $body = preg_replace('/((^|\n)>*)/', '\\1> ', trim($body));
519 $body = '<pre>' . htmlspecialchars($body, $this->htmlFuncFlags
, self
::ENCODING
) . '</pre>';
521 $this->options
['width'] = $pWidth;
523 $text = mb_substr($text, 0, $start - $diff)
525 . mb_substr($text, $end +
mb_strlen($m[0]) - $diff);
527 $diff +
= $len +
$taglen +
mb_strlen($m[0]) - mb_strlen($body);
533 $taglen = mb_strlen($m[0]);
542 * Callback function for preg_replace_callback use.
544 * @param array $matches PREG matches
547 protected function pregCallback($matches)
549 switch (mb_strtolower($matches[1])) {
551 // Replace newlines with spaces.
552 $para = str_replace("\n", " ", $matches[3]);
554 // Trim trailing and leading whitespace within the tag.
557 // Add trailing newlines for this para.
558 return "\n" . $para . "\n";
563 return $this->toupper($matches[3]);
565 return $this->toupper("\t\t" . $matches[3] . "\n");
567 return $this->toupper("\n\n" . $matches[3] . "\n\n");
569 // override the link method
570 $linkOverride = null;
571 if (preg_match('/_html2text_link_(\w+)/', $matches[4], $linkOverrideMatch)) {
572 $linkOverride = $linkOverrideMatch[1];
574 // Remove spaces in URL (#1487805)
575 $url = str_replace(' ', '', $matches[3]);
577 return $this->buildlinkList($url, $matches[5], $linkOverride);
584 * Callback function for preg_replace_callback use in PRE content handler.
586 * @param array $matches PREG matches
589 protected function pregPreCallback(/** @noinspection PhpUnusedParameterInspection */ $matches)
591 return $this->preContent
;
595 * Strtoupper function with HTML tags and entities handling.
597 * @param string $str Text to convert
598 * @return string Converted text
600 protected function toupper($str)
602 // string can contain HTML tags
603 $chunks = preg_split('/(<[^>]*>)/', $str, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE
);
605 // convert toupper only the text between HTML tags
606 foreach ($chunks as $i => $chunk) {
607 if ($chunk[0] != '<') {
608 $chunks[$i] = $this->strtoupper($chunk);
612 return implode($chunks);
616 * Strtoupper multibyte wrapper function with HTML entities handling.
618 * @param string $str Text to convert
619 * @return string Converted text
621 protected function strtoupper($str)
623 $str = html_entity_decode($str, $this->htmlFuncFlags
, self
::ENCODING
);
624 $str = mb_strtoupper($str);
625 $str = htmlspecialchars($str, $this->htmlFuncFlags
, self
::ENCODING
);