html_to_text.inc 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743
  1. <?php
  2. /**
  3. * @file
  4. * Copy of drupal_html_to_text improvements from issue #299138.
  5. */
  6. /**
  7. * Perform format=flowed soft wrapping for mail (RFC 3676).
  8. *
  9. * We use delsp=yes wrapping, but only break non-spaced languages when
  10. * absolutely necessary to avoid compatibility issues.
  11. *
  12. * We deliberately use variable_get('mail_line_endings', MAIL_LINE_ENDINGS)
  13. * rather than "\r\n".
  14. *
  15. * @param $text
  16. * The plain text to process.
  17. * @param array $options
  18. * (optional) An array containing one or more of the following keys:
  19. * - indent: A string to indent the text with. Only '>' characters are
  20. * repeated on subsequent wrapped lines. Others are replaced by spaces.
  21. * - max: The maximum length at which to wrap each line. Defaults to 80.
  22. * - stuff: Whether to space-stuff special lines. Defaults to TRUE.
  23. * - hard: Whether to enforce the maximum line length even if no convenient
  24. * space character is available. Defaults to FALSE.
  25. * - pad: A string to use for padding short lines to 'max' characters. If
  26. * more than one character, only the last will be repeated.
  27. * - break: The line break sequence to insert. The default is one of the
  28. * following:
  29. * - "\r\n": Windows, when $text does not contain a space character.
  30. * - "\n": Non-Windows, when $text does not contain a space character.
  31. * - " \r\n": On Windows, when $text contains at least one space.
  32. * - " \n": Non-Windows, when $text contains at least one space.
  33. *
  34. * @see drupal_mail()
  35. */
  36. function mailsystem_wrap_mail($text, array $options = array()) {
  37. static $defaults;
  38. if (!isset($defaults)) {
  39. $defaults = array(
  40. 'indent' => '',
  41. 'pad' => '',
  42. 'pad_repeat' => '',
  43. 'max' => 80,
  44. 'stuff' => TRUE,
  45. 'hard' => FALSE,
  46. 'eol' => variable_get('mail_line_endings', MAIL_LINE_ENDINGS),
  47. );
  48. }
  49. $options += $defaults;
  50. if (!isset($options['break'])) {
  51. // Allow soft-wrap spaces only when $text contains at least one space.
  52. $options['break'] = (strpos($text, ' ') === FALSE ? '' : ' ') . $defaults['eol'];
  53. }
  54. $options['wrap'] = $options['max'] - drupal_strlen($options['indent']);
  55. if ($options['pad']) {
  56. $options['pad_repeat'] = drupal_substr($options['pad'], -1, 1);
  57. }
  58. // The 'clean' indent is applied to all lines after the first one.
  59. $options['clean'] = _mailsystem_html_to_text_clean($options['indent']);
  60. // Wrap lines according to RFC 3676.
  61. $lines = explode($defaults['eol'], $text);
  62. array_walk($lines, '_mailsystem_wrap_mail_line', $options);
  63. // Expand the lines array on newly-inserted line breaks.
  64. $lines = explode($defaults['eol'], implode($defaults['eol'], $lines));
  65. // Apply indentation, space-stuffing, and padding.
  66. array_walk($lines, '_mailsystem_indent_mail_line', $options);
  67. return implode($defaults['eol'], $lines);
  68. }
  69. /**
  70. * Transform an HTML string into plain text, preserving the structure of the
  71. * markup. Useful for preparing the body of a node to be sent by e-mail.
  72. *
  73. * The output will be suitable for use as 'format=flowed; delsp=yes' text
  74. * (RFC 3676) and can be passed directly to drupal_mail() for sending.
  75. *
  76. * We deliberately use variable_get('mail_line_endings', MAIL_LINE_ENDINGS)
  77. * rather than "\r\n".
  78. *
  79. * This function provides suitable alternatives for the following tags:
  80. *
  81. * <a> <address> <b> <blockquote> <br /> <caption> <cite> <dd> <div> <dl> <dt>
  82. * <em> <h1> <h2> <h3> <h4> <h5> <h6> <hr /> <i> <li> <ol> <p> <pre> <strong>
  83. * <table> <tbody> <td> <tfoot> <thead> <tr> <u> <ul>
  84. *
  85. * The following tag attributes are supported:
  86. * - <a href=...>: Hyperlink destination urls.
  87. * - <li value=...>: Ordered list item numbers.
  88. * - <ol start=...>: Ordered list start number.
  89. *
  90. * @param $string
  91. * The string to be transformed.
  92. * @param $allowed_tags
  93. * (optional) If supplied, a list of tags that will be transformed. If
  94. * omitted, all supported tags are transformed.
  95. *
  96. * @return
  97. * The transformed string.
  98. *
  99. * @see drupal_mail()
  100. */
  101. function mailsystem_html_to_text($string, $allowed_tags = NULL) {
  102. $eol = variable_get('mail_line_endings', MAIL_LINE_ENDINGS);
  103. // Cache list of supported tags.
  104. static $supported_tags;
  105. if (!isset($supported_tags)) {
  106. $supported_tags = array(
  107. 'a', 'address', 'b', 'blockquote', 'br', 'cite', 'dd', 'div', 'dl',
  108. 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'li',
  109. 'ol', 'p', 'pre', 'strong', 'table', 'td', 'tr', 'u', 'ul',
  110. );
  111. }
  112. // Make sure only supported tags are kept.
  113. $allowed_tags = isset($allowed_tags) ? array_intersect($supported_tags, $allowed_tags) : $supported_tags;
  114. // Parse $string into a DOM tree.
  115. $dom = filter_dom_load($string);
  116. $notes = array();
  117. // Recursively convert the DOM tree into plain text.
  118. $text = _mailsystem_html_to_text($dom->documentElement, $allowed_tags, $notes);
  119. // Hard-wrap at 1000 characters (including the line break sequence)
  120. // and space-stuff special lines.
  121. $text = mailsystem_wrap_mail($text, array('max' => 1000 - strlen($eol), 'hard' => TRUE));
  122. // Change non-breaking spaces back to regular spaces, and trim line breaks.
  123. // chr(160) is the non-breaking space character.
  124. $text = str_replace(chr(160), ' ', trim($text, $eol));
  125. // Add footnotes;
  126. if ($notes) {
  127. // Add a blank line before the footnote list.
  128. $text .= $eol;
  129. foreach ($notes as $url => $note) {
  130. $text .= $eol . '[' . $note . '] ' . $url;
  131. }
  132. }
  133. return $text;
  134. }
  135. /**
  136. * Helper function for drupal_html_to_text().
  137. *
  138. * Recursively converts $node to text, wrapping and indenting as necessary.
  139. *
  140. * @param $node
  141. * The source DOMNode.
  142. * @param $allowed_tags
  143. * A list of tags that will be transformed.
  144. * @param array &$notes
  145. * A writeable array of footnote reference numbers, keyed by their
  146. * respective hyperlink destination urls.
  147. * @param $line_length
  148. * The maximum length of a line, for wrapping. Defaults to 80 characters.
  149. * @param array $parents
  150. * The list of ancestor tags, from nearest to most distant. Defaults to an
  151. * empty array().
  152. * @param $count
  153. * The number to use for the next list item within an ordered list. Defaults
  154. * to 1.
  155. */
  156. function _mailsystem_html_to_text(DOMNode $node, array $allowed_tags, array &$notes, $line_length = 80, array $parents = array(), &$count = NULL) {
  157. if (!isset($count)) {
  158. $count = 1;
  159. }
  160. $eol = variable_get('mail_line_endings', MAIL_LINE_ENDINGS);
  161. if ($node->nodeType === XML_TEXT_NODE) {
  162. // For text nodes, we just copy the text content.
  163. $text = $node->textContent;
  164. // Convert line breaks and trim trailing spaces.
  165. $text = preg_replace('/ *\r?\n/', $eol, $text);
  166. if (in_array('pre', $parents)) {
  167. // Within <pre> tags, all spaces become non-breaking.
  168. // chr(160) is the non-breaking space character.
  169. $text = str_replace(' ', chr(160), $text);
  170. }
  171. else {
  172. // Outside <pre> tags, collapse whitespace.
  173. $text = preg_replace('/[[:space:]]+/', ' ', $text);
  174. }
  175. return $text;
  176. }
  177. // Non-text node.
  178. $tag = '';
  179. $text = '';
  180. $child_text = '';
  181. $child_count = 1;
  182. $indent = '';
  183. $prefix = '';
  184. $suffix = '';
  185. $pad = '';
  186. if (isset($node->tagName) && in_array($node->tagName, $allowed_tags)) {
  187. $tag = $node->tagName;
  188. switch ($tag) {
  189. // Turn links with valid hrefs into footnotes.
  190. case 'a':
  191. $test = !empty($node->attributes);
  192. $test = $test && ($href = $node->attributes->getNamedItem('href'));
  193. $test = $test && ($url = url(preg_replace('|^' . base_path() . '|', '', $href->nodeValue), array('absolute' => TRUE)));
  194. $test = $test && valid_url($url);
  195. if ($test) {
  196. // Only add links that have not already been added.
  197. if (isset($notes[$url])) {
  198. $note = $notes[$url];
  199. }
  200. else {
  201. $note = count($notes) + 1;
  202. $notes[$url] = $note;
  203. }
  204. $suffix = ' [' . $note . ']';
  205. }
  206. break;
  207. // Generic block-level tags.
  208. case 'address':
  209. case 'caption':
  210. case 'div':
  211. case 'p':
  212. case 'pre':
  213. // Start on a new line except as the first child of a list item.
  214. if (!isset($parents[0]) || $parents[0] !== 'li' || !$node->isSameNode($node->parentNode->firstChild)) {
  215. $text = $eol;
  216. }
  217. $suffix = $eol;
  218. break;
  219. // Forced line break.
  220. case 'br':
  221. $text = $eol;
  222. break;
  223. // Boldface by wrapping with "*" characters.
  224. case 'b':
  225. case 'strong':
  226. $prefix = '*';
  227. $suffix = '*';
  228. break;
  229. // Italicize by wrapping with "/" characters.
  230. case 'cite':
  231. case 'em':
  232. case 'i':
  233. $prefix = '/';
  234. $suffix = '/';
  235. break;
  236. // Underline by wrapping with "_" characters.
  237. case 'u':
  238. $prefix = '_';
  239. $suffix = '_';
  240. break;
  241. // Blockquotes are indented by "> " at each level.
  242. case 'blockquote':
  243. $text = $eol;
  244. // chr(160) is the non-breaking space character.
  245. $indent = '>' . chr(160);
  246. $suffix = $eol;
  247. break;
  248. // Dictionary definitions are indented by four spaces.
  249. case 'dd':
  250. // chr(160) is the non-breaking space character.
  251. $indent = chr(160) . chr(160) . chr(160) . chr(160);
  252. $suffix = $eol;
  253. break;
  254. // Dictionary list.
  255. case 'dl':
  256. // Start on a new line as the first child of a list item.
  257. if (!isset($parents[0]) || $parents[0] !== 'li' || !$node->isSameNode($node->parentNode->firstChild)) {
  258. $text = $eol;
  259. }
  260. $suffix = $eol;
  261. break;
  262. // Dictionary term.
  263. case 'dt':
  264. $suffix = $eol;
  265. break;
  266. // Header level 1 is prefixed by eight "=" characters.
  267. case 'h1':
  268. $text = "$eol$eol";
  269. // chr(160) is the non-breaking space character.
  270. $indent = '========' . chr(160);
  271. $pad = chr(160) . '=';
  272. $suffix = $eol;
  273. break;
  274. // Header level 2 is prefixed by six "-" characters.
  275. case 'h2':
  276. $text = "$eol$eol";
  277. // chr(160) is the non-breaking space character.
  278. $indent = '------' . chr(160);
  279. $pad = chr(160) . '-';
  280. $suffix = $eol;
  281. break;
  282. // Header level 3 is prefixed by four "." characters and a space.
  283. case 'h3':
  284. $text = "$eol$eol";
  285. // chr(160) is the non-breaking space character.
  286. $indent = '....' . chr(160);
  287. $suffix = $eol;
  288. break;
  289. // Header level 4 is prefixed by three "." characters and a space.
  290. case 'h4':
  291. $text = "$eol$eol";
  292. // chr(160) is the non-breaking space character.
  293. $indent = '...' . chr(160);
  294. $suffix = $eol;
  295. break;
  296. // Header level 5 is prefixed by two "." character and a space.
  297. case 'h5':
  298. $text = "$eol$eol";
  299. // chr(160) is the non-breaking space character.
  300. $indent = '..' . chr(160);
  301. $suffix = $eol;
  302. break;
  303. // Header level 6 is prefixed by one "." character and a space.
  304. case 'h6':
  305. $text = "$eol$eol";
  306. // chr(160) is the non-breaking space character.
  307. $indent = '.' . chr(160);
  308. $suffix = $eol;
  309. break;
  310. // Horizontal rulers become a line of "-" characters.
  311. case 'hr':
  312. $text = $eol;
  313. $child_text = '-';
  314. $pad = '-';
  315. $suffix = $eol;
  316. break;
  317. // List items are treated differently depending on the parent tag.
  318. case 'li':
  319. // Ordered list item.
  320. if (reset($parents) === 'ol') {
  321. // Check the value attribute.
  322. $test = !empty($node->attributes);
  323. $test = $test && ($value = $node->attributes->getNamedItem('value'));
  324. if ($test) {
  325. $count = $value->nodeValue;
  326. }
  327. // chr(160) is the non-breaking space character.
  328. $indent = ($count < 10 ? chr(160) : '') . chr(160) . "$count)" . chr(160);
  329. $count++;
  330. }
  331. // Unordered list item.
  332. else {
  333. // chr(160) is the non-breaking space character.
  334. $indent = chr(160) . '*' . chr(160);
  335. }
  336. $suffix = $eol;
  337. break;
  338. // Ordered lists.
  339. case 'ol':
  340. // Start on a new line as the first child of a list item.
  341. if (!isset($parents[0]) || $parents[0] !== 'li' || !$node->isSameNode($node->parentNode->firstChild)) {
  342. $text = $eol;
  343. }
  344. // Check the start attribute.
  345. $test = !empty($node->attributes);
  346. $test = $test && ($value = $node->attributes->getNamedItem('start'));
  347. if ($test) {
  348. $child_count = $value->nodeValue;
  349. }
  350. break;
  351. // Tables require special handling.
  352. case 'table':
  353. return _mailsystem_html_to_text_table($node, $allowed_tags, $notes, $line_length);
  354. // Separate adjacent table cells by two non-breaking spaces.
  355. case 'td':
  356. if (!empty($node->nextSibling)) {
  357. // chr(160) is the non-breaking space character.
  358. $suffix = chr(160) . chr(160);
  359. }
  360. break;
  361. // End each table row with a newline.
  362. case 'tr':
  363. $suffix = $eol;
  364. break;
  365. // Unordered lists.
  366. case 'ul':
  367. // Start on a new line as the first child of a list item.
  368. if (!isset($parents[0]) || $parents[0] !== 'li' || !$node->isSameNode($node->parentNode->firstChild)) {
  369. $text = $eol;
  370. }
  371. break;
  372. default:
  373. // Coder review complains if there is no default case.
  374. break;
  375. }
  376. // Only add allowed tags to the $parents array.
  377. array_unshift($parents, $tag);
  378. }
  379. // Copy each child node to output.
  380. if ($node->hasChildNodes()) {
  381. foreach ($node->childNodes as $child) {
  382. $child_text .= _mailsystem_html_to_text($child, $allowed_tags, $notes, $line_length - drupal_strlen($indent), $parents, $child_count);
  383. }
  384. }
  385. // We only add prefix and suffix if the child nodes were non-empty.
  386. if ($child_text > '') {
  387. // We capitalize the contents of h1 and h2 tags.
  388. if ($tag === 'h1' || $tag === 'h2') {
  389. $child_text = drupal_strtoupper($child_text);
  390. }
  391. // Don't add a newline to an existing newline.
  392. if ($suffix === $eol && drupal_substr($child_text, -drupal_strlen($eol)) === $eol) {
  393. $suffix = '';
  394. }
  395. // Trim spaces around newlines except with <pre> or inline tags.
  396. if (!in_array($tag, array('a', 'b', 'cite', 'em', 'i', 'pre', 'strong', 'u'))) {
  397. $child_text = preg_replace('/ *' . $eol . ' */', $eol, $child_text);
  398. }
  399. // Soft-wrap at effective line length, but don't space-stuff.
  400. $child_text = mailsystem_wrap_mail(
  401. $prefix . $child_text,
  402. array(
  403. // chr(160) is the non-breaking space character.
  404. 'break' => chr(160) . $eol,
  405. 'indent' => $indent,
  406. 'max' => $line_length,
  407. 'pad' => $pad,
  408. 'stuff' => FALSE,
  409. )
  410. ) . $suffix;
  411. if ($tag === 'pre') {
  412. // Perform RFC-3676 soft-wrapping.
  413. // chr(160) is the non-breaking space character.
  414. $child_text = str_replace(chr(160), ' ', $child_text);
  415. $child_text = mailsystem_wrap_mail(
  416. $child_text,
  417. array('max' => $line_length, 'stuff' => FALSE)
  418. );
  419. // chr(160) is the non-breaking space character.
  420. $child_text = str_replace(' ', chr(160), $child_text);
  421. }
  422. $text .= $child_text;
  423. }
  424. return $text;
  425. }
  426. /**
  427. * Helper function for _mailsystem_html_to_text().
  428. *
  429. * Renders a <table> DOM Node into plain text. Attributes such as rowspan,
  430. * colspan, padding, border, etc. are ignored.
  431. *
  432. * @param DOMNode $node
  433. * The DOMNode corresponding to the <table> tag and its contents.
  434. * @param $allowed_tags
  435. * The list of allowed tags passed to _mailsystem_html_to_text().
  436. * @param array &$notes
  437. * A writeable array of footnote reference numbers, keyed by their
  438. * respective hyperlink destination urls.
  439. * @param $table_width
  440. * The desired maximum table width, after word-wrapping each table cell.
  441. *
  442. * @return
  443. * A plain text representation of the table.
  444. *
  445. * @see _mailsystem_html_to_text()
  446. */
  447. function _mailsystem_html_to_text_table(DOMNode $node, $allowed_tags = NULL, array &$notes = array(), $table_width = 80) {
  448. $eol = variable_get('mail_line_endings', MAIL_LINE_ENDINGS);
  449. $header = array();
  450. $footer = array();
  451. $body = array();
  452. $text = $eol;
  453. $current = $node;
  454. while (TRUE) {
  455. if (isset($current->tagName)) {
  456. switch ($current->tagName) {
  457. case 'caption': // The table caption is added first.
  458. $text = _mailsystem_html_to_text($current, $allowed_tags, $notes, $table_width);
  459. break;
  460. case 'tr':
  461. switch ($current->parentNode->tagName) {
  462. case 'thead':
  463. $header[] = $current;
  464. break;
  465. case 'tfoot':
  466. $footer[] = $current;
  467. break;
  468. default: // Either 'tbody' or 'table'.
  469. $body[] = $current;
  470. break;
  471. }
  472. break;
  473. default:
  474. if ($current->hasChildNodes()) {
  475. $current = $current->firstChild;
  476. continue 2;
  477. }
  478. }
  479. }
  480. do {
  481. if ($current->nextSibling) {
  482. $current = $current->nextSibling;
  483. continue 2;
  484. }
  485. $current = $current->parentNode;
  486. } while ($current && !$current->isSameNode($node));
  487. break;
  488. }
  489. // Merge the thead, tbody, and tfoot sections together.
  490. if ($rows = array_merge($header, $body, $footer)) {
  491. $num_rows = count($rows);
  492. // First just count the number of columns.
  493. $num_cols = 0;
  494. foreach ($rows as $row) {
  495. $row_cols = 0;
  496. foreach ($row->childNodes as $cell) {
  497. if (isset($cell->tagName) && in_array($cell->tagName, array('td', 'th'))) {
  498. $row_cols++;
  499. }
  500. }
  501. $num_cols = max($num_cols, $row_cols);
  502. }
  503. // If any columns were found, calculate each column height and width.
  504. if ($num_cols) {
  505. // Set up a binary search for best wrap width for each column.
  506. $max = max($table_width - $num_cols - 1, 1);
  507. $max_wraps = array_fill(0, $num_cols, $max);
  508. $try = max(intval(($table_width - 1) / $num_cols - 1), 1);
  509. $try_wraps = array_fill(0, $num_cols, $try);
  510. $min_wraps = array_fill(0, $num_cols, 1);
  511. // Start searching...
  512. $change = FALSE;
  513. do {
  514. $change = FALSE;
  515. $widths = array_fill(0, $num_cols, 0);
  516. $heights = array_fill(0, $num_rows, 0);
  517. $table = array_fill(0, $num_rows, array_fill(0, $num_cols, ''));
  518. $breaks = array_fill(0, $num_cols, FALSE);
  519. foreach ($rows as $i => $row) {
  520. $j = 0;
  521. foreach ($row->childNodes as $cell) {
  522. if (!isset($cell->tagName) || !in_array($cell->tagName, array('td', 'th'))) {
  523. // Skip text nodes.
  524. continue;
  525. }
  526. // Render the cell contents.
  527. $cell = _mailsystem_html_to_text($cell, $allowed_tags, $notes, $try_wraps[$j]);
  528. // Trim leading line-breaks and trailing whitespace.
  529. // chr(160) is the non-breaking space character.
  530. $cell = rtrim(ltrim($cell, $eol), ' ' . $eol . chr(160));
  531. $table[$i][$j] = $cell;
  532. if ($cell > '') {
  533. // Split the cell into lines.
  534. $lines = explode($eol, $cell);
  535. // The row height is the maximum number of lines among all the
  536. // cells in that row.
  537. $heights[$i] = max($heights[$i], count($lines));
  538. foreach ($lines as $line) {
  539. $this_width = drupal_strlen($line);
  540. // The column width is the maximum line width among all the
  541. // lines in that column.
  542. if ($this_width > $widths[$j]) {
  543. $widths[$j] = $this_width;
  544. // If the longest line in a column contains at least one
  545. // space character, then the table can be made narrower.
  546. $breaks[$j] = strpos(' ', $line) !== FALSE;
  547. }
  548. }
  549. }
  550. $j++;
  551. }
  552. }
  553. // Calculate the total table width;
  554. $this_width = array_sum($widths) + $num_cols + 1;
  555. if ($this_width > $table_width) {
  556. // Wider than desired.
  557. if (!in_array(TRUE, $breaks)) {
  558. // If there are no more break points, then the table is already as
  559. // narrow as it can get, so we're done.
  560. break;
  561. }
  562. foreach ($try_wraps as $i => $wrap) {
  563. $max_wraps[$i] = min($max_wraps[$i], $wrap);
  564. if ($breaks[$i]) {
  565. $new_wrap = intval(($min_wraps[$i] + $max_wraps[$i]) / 2);
  566. $new_wrap = min($new_wrap, $widths[$i] - 1);
  567. $new_wrap = max($new_wrap, $min_wraps[$i]);
  568. }
  569. else {
  570. // There's no point in trying to make the column narrower than
  571. // the widest un-wrappable line in the column.
  572. $min_wraps[$i] = $widths[$i];
  573. $new_wrap = $widths[$i];
  574. }
  575. if ($try_wraps[$i] > $new_wrap) {
  576. $try_wraps[$i] = $new_wrap;
  577. $change = TRUE;
  578. }
  579. }
  580. }
  581. elseif ($this_width < $table_width) {
  582. // Narrower than desired.
  583. foreach ($try_wraps as $i => $wrap) {
  584. if ($min_wraps[$i] < $wrap) {
  585. $min_wraps[$i] = $wrap;
  586. }
  587. $new_wrap = intval(($min_wraps[$i] + $max_wraps[$i]) / 2);
  588. $new_wrap = max($new_wrap, $widths[$i] + 1);
  589. $new_wrap = min($new_wrap, $max_wraps[$i]);
  590. if ($try_wraps[$i] < $new_wrap) {
  591. $try_wraps[$i] = $new_wrap;
  592. $change = TRUE;
  593. }
  594. }
  595. }
  596. } while ($change);
  597. // Pad each cell to column width and line height.
  598. for ($i = 0; $i < $num_rows; $i++) {
  599. if ($heights[$i]) {
  600. for ($j = 0; $j < $num_cols; $j++) {
  601. $cell = $table[$i][$j];
  602. // Pad each cell to the maximum number of lines in that row.
  603. $lines = array_pad(explode($eol, $cell), $heights[$i], '');
  604. foreach ($lines as $k => $line) {
  605. // Pad each line to the maximum width in that column.
  606. $repeat = $widths[$j] - drupal_strlen($line);
  607. if ($repeat > 0) {
  608. // chr(160) is the non-breaking space character.
  609. $lines[$k] .= str_repeat(chr(160), $repeat);
  610. }
  611. }
  612. $table[$i][$j] = $lines;
  613. }
  614. }
  615. }
  616. // Generate the row separator line.
  617. $separator = '+';
  618. for ($i = 0; $i < $num_cols; $i++) {
  619. $separator .= str_repeat('-', $widths[$i]) . '+';
  620. }
  621. $separator .= $eol;
  622. for ($i = 0; $i < $num_rows; $i++) {
  623. $text .= $separator;
  624. if (!$heights[$i]) {
  625. continue;
  626. }
  627. $row = $table[$i];
  628. // For each row, iterate first by lines within the row.
  629. for ($k = 0; $k < $heights[$i]; $k++) {
  630. // Add a vertical-bar at the beginning of each row line.
  631. $row_line = '|';
  632. $trimmed = '';
  633. // Within each row line, iterate by cells within that line.
  634. for ($j = 0; $j < $num_cols; $j++) {
  635. // Add a vertical bar at the end of each cell line.
  636. $row_line .= $row[$j][$k] . '|';
  637. // chr(160) is the non-breaking space character.
  638. $trimmed .= trim($row[$j][$k], ' ' . $eol . chr(160));
  639. }
  640. if ($trimmed > '') {
  641. // Only print rows that are non-empty.
  642. $text .= $row_line . $eol;
  643. }
  644. }
  645. }
  646. // Final output ends with a row separator.
  647. $text .= $separator;
  648. }
  649. }
  650. // Make sure formatted table content doesn't line-wrap.
  651. // chr(160) is the non-breaking space character.
  652. return str_replace(' ', chr(160), $text);
  653. }
  654. /**
  655. * Helper function for array_walk in drupal_wrap_mail().
  656. *
  657. * Inserts $values['break'] sequences to break up $line into parts of no more
  658. * than $values['wrap'] characters. Only breaks at space characters, unless
  659. * $values['hard'] is TRUE.
  660. */
  661. function _mailsystem_wrap_mail_line(&$line, $key, $values) {
  662. $line = wordwrap($line, $values['wrap'], $values['break'], $values['hard']);
  663. }
  664. /**
  665. * Helper function for array_walk in drupal_wrap_mail().
  666. *
  667. * If $values['pad'] is non-empty, $values['indent'] will be added at the start
  668. * of each line, and $values['pad'] at the end, repeating the last character of
  669. * $values['pad'] until the line length equals $values['max'].
  670. *
  671. * If $values['pad'] is empty, $values['indent'] will be added at the start of
  672. * the first line, and $values['clean'] at the start of subsequent lines.
  673. *
  674. * If $values['stuff'] is true, then an extra space character will be added at
  675. * the start of any line beginning with a space, a '>', or the word 'From'.
  676. *
  677. * @see http://www.ietf.org/rfc/rfc3676.txt
  678. */
  679. function _mailsystem_indent_mail_line(&$line, $key, $values) {
  680. if ($line == '') {
  681. return;
  682. }
  683. if ($values['pad']) {
  684. $line = $values['indent'] . $line;
  685. $count = $values['max'] - drupal_strlen($line) - drupal_strlen($values['pad']);
  686. if ($count >= 0) {
  687. $line .= $values['pad'] . str_repeat($values['pad_repeat'], $count);
  688. }
  689. }
  690. else {
  691. $line = $values[$key === 0 ? 'indent' : 'clean'] . $line;
  692. }
  693. if ($values['stuff']) {
  694. // chr(160) is the non-breaking space character.
  695. $line = preg_replace('/^(' . chr(160) . '| |>|From)/', ' $1', $line);
  696. }
  697. }
  698. /**
  699. * Helper function for drupal_wrap_mail() and drupal_html_to_text().
  700. *
  701. * Replace all non-quotation markers from a given piece of indentation with
  702. * non-breaking space characters.
  703. */
  704. function _mailsystem_html_to_text_clean($indent) {
  705. // chr(160) is the non-breaking space character.
  706. return preg_replace('/[^>]/', chr(160), $indent);
  707. }