' characters are * repeated on subsequent wrapped lines. Others are replaced by spaces. * - max: The maximum length at which to wrap each line. Defaults to 80. * - stuff: Whether to space-stuff special lines. Defaults to TRUE. * - hard: Whether to enforce the maximum line length even if no convenient * space character is available. Defaults to FALSE. * - pad: A string to use for padding short lines to 'max' characters. If * more than one character, only the last will be repeated. * - break: The line break sequence to insert. The default is one of the * following: * - "\r\n": Windows, when $text does not contain a space character. * - "\n": Non-Windows, when $text does not contain a space character. * - " \r\n": On Windows, when $text contains at least one space. * - " \n": Non-Windows, when $text contains at least one space. * * @see drupal_mail() */ function mailsystem_wrap_mail($text, array $options = array()) { static $defaults; if (!isset($defaults)) { $defaults = array( 'indent' => '', 'pad' => '', 'pad_repeat' => '', 'max' => 80, 'stuff' => TRUE, 'hard' => FALSE, 'eol' => variable_get('mail_line_endings', MAIL_LINE_ENDINGS), ); } $options += $defaults; if (!isset($options['break'])) { // Allow soft-wrap spaces only when $text contains at least one space. $options['break'] = (strpos($text, ' ') === FALSE ? '' : ' ') . $defaults['eol']; } $options['wrap'] = $options['max'] - drupal_strlen($options['indent']); if ($options['pad']) { $options['pad_repeat'] = drupal_substr($options['pad'], -1, 1); } // The 'clean' indent is applied to all lines after the first one. $options['clean'] = _mailsystem_html_to_text_clean($options['indent']); // Wrap lines according to RFC 3676. $lines = explode($defaults['eol'], $text); array_walk($lines, '_mailsystem_wrap_mail_line', $options); // Expand the lines array on newly-inserted line breaks. $lines = explode($defaults['eol'], implode($defaults['eol'], $lines)); // Apply indentation, space-stuffing, and padding. array_walk($lines, '_mailsystem_indent_mail_line', $options); return implode($defaults['eol'], $lines); } /** * Transform an HTML string into plain text, preserving the structure of the * markup. Useful for preparing the body of a node to be sent by e-mail. * * The output will be suitable for use as 'format=flowed; delsp=yes' text * (RFC 3676) and can be passed directly to drupal_mail() for sending. * * We deliberately use variable_get('mail_line_endings', MAIL_LINE_ENDINGS) * rather than "\r\n". * * This function provides suitable alternatives for the following tags: * *

*


    1.  
       *  
      DOM Node into plain text. Attributes such as rowspan, * colspan, padding, border, etc. are ignored. * * @param DOMNode $node * The DOMNode corresponding to the
      tag and its contents. * @param $allowed_tags * The list of allowed tags passed to _mailsystem_html_to_text(). * @param array &$notes * A writeable array of footnote reference numbers, keyed by their * respective hyperlink destination urls. * @param $table_width * The desired maximum table width, after word-wrapping each table cell. * * @return * A plain text representation of the table. * * @see _mailsystem_html_to_text() */ function _mailsystem_html_to_text_table(DOMNode $node, $allowed_tags = NULL, array &$notes = array(), $table_width = 80) { $eol = variable_get('mail_line_endings', MAIL_LINE_ENDINGS); $header = array(); $footer = array(); $body = array(); $text = $eol; $current = $node; while (TRUE) { if (isset($current->tagName)) { switch ($current->tagName) { case 'caption': // The table caption is added first. $text = _mailsystem_html_to_text($current, $allowed_tags, $notes, $table_width); break; case 'tr': switch ($current->parentNode->tagName) { case 'thead': $header[] = $current; break; case 'tfoot': $footer[] = $current; break; default: // Either 'tbody' or 'table'. $body[] = $current; break; } break; default: if ($current->hasChildNodes()) { $current = $current->firstChild; continue 2; } } } do { if ($current->nextSibling) { $current = $current->nextSibling; continue 2; } $current = $current->parentNode; } while ($current && !$current->isSameNode($node)); break; } // Merge the thead, tbody, and tfoot sections together. if ($rows = array_merge($header, $body, $footer)) { $num_rows = count($rows); // First just count the number of columns. $num_cols = 0; foreach ($rows as $row) { $row_cols = 0; foreach ($row->childNodes as $cell) { if (isset($cell->tagName) && in_array($cell->tagName, array('td', 'th'))) { $row_cols++; } } $num_cols = max($num_cols, $row_cols); } // If any columns were found, calculate each column height and width. if ($num_cols) { // Set up a binary search for best wrap width for each column. $max = max($table_width - $num_cols - 1, 1); $max_wraps = array_fill(0, $num_cols, $max); $try = max(intval(($table_width - 1) / $num_cols - 1), 1); $try_wraps = array_fill(0, $num_cols, $try); $min_wraps = array_fill(0, $num_cols, 1); // Start searching... $change = FALSE; do { $change = FALSE; $widths = array_fill(0, $num_cols, 0); $heights = array_fill(0, $num_rows, 0); $table = array_fill(0, $num_rows, array_fill(0, $num_cols, '')); $breaks = array_fill(0, $num_cols, FALSE); foreach ($rows as $i => $row) { $j = 0; foreach ($row->childNodes as $cell) { if (!isset($cell->tagName) || !in_array($cell->tagName, array('td', 'th'))) { // Skip text nodes. continue; } // Render the cell contents. $cell = _mailsystem_html_to_text($cell, $allowed_tags, $notes, $try_wraps[$j]); // Trim leading line-breaks and trailing whitespace. // chr(160) is the non-breaking space character. $cell = rtrim(ltrim($cell, $eol), ' ' . $eol . chr(160)); $table[$i][$j] = $cell; if ($cell > '') { // Split the cell into lines. $lines = explode($eol, $cell); // The row height is the maximum number of lines among all the // cells in that row. $heights[$i] = max($heights[$i], count($lines)); foreach ($lines as $line) { $this_width = drupal_strlen($line); // The column width is the maximum line width among all the // lines in that column. if ($this_width > $widths[$j]) { $widths[$j] = $this_width; // If the longest line in a column contains at least one // space character, then the table can be made narrower. $breaks[$j] = strpos(' ', $line) !== FALSE; } } } $j++; } } // Calculate the total table width; $this_width = array_sum($widths) + $num_cols + 1; if ($this_width > $table_width) { // Wider than desired. if (!in_array(TRUE, $breaks)) { // If there are no more break points, then the table is already as // narrow as it can get, so we're done. break; } foreach ($try_wraps as $i => $wrap) { $max_wraps[$i] = min($max_wraps[$i], $wrap); if ($breaks[$i]) { $new_wrap = intval(($min_wraps[$i] + $max_wraps[$i]) / 2); $new_wrap = min($new_wrap, $widths[$i] - 1); $new_wrap = max($new_wrap, $min_wraps[$i]); } else { // There's no point in trying to make the column narrower than // the widest un-wrappable line in the column. $min_wraps[$i] = $widths[$i]; $new_wrap = $widths[$i]; } if ($try_wraps[$i] > $new_wrap) { $try_wraps[$i] = $new_wrap; $change = TRUE; } } } elseif ($this_width < $table_width) { // Narrower than desired. foreach ($try_wraps as $i => $wrap) { if ($min_wraps[$i] < $wrap) { $min_wraps[$i] = $wrap; } $new_wrap = intval(($min_wraps[$i] + $max_wraps[$i]) / 2); $new_wrap = max($new_wrap, $widths[$i] + 1); $new_wrap = min($new_wrap, $max_wraps[$i]); if ($try_wraps[$i] < $new_wrap) { $try_wraps[$i] = $new_wrap; $change = TRUE; } } } } while ($change); // Pad each cell to column width and line height. for ($i = 0; $i < $num_rows; $i++) { if ($heights[$i]) { for ($j = 0; $j < $num_cols; $j++) { $cell = $table[$i][$j]; // Pad each cell to the maximum number of lines in that row. $lines = array_pad(explode($eol, $cell), $heights[$i], ''); foreach ($lines as $k => $line) { // Pad each line to the maximum width in that column. $repeat = $widths[$j] - drupal_strlen($line); if ($repeat > 0) { // chr(160) is the non-breaking space character. $lines[$k] .= str_repeat(chr(160), $repeat); } } $table[$i][$j] = $lines; } } } // Generate the row separator line. $separator = '+'; for ($i = 0; $i < $num_cols; $i++) { $separator .= str_repeat('-', $widths[$i]) . '+'; } $separator .= $eol; for ($i = 0; $i < $num_rows; $i++) { $text .= $separator; if (!$heights[$i]) { continue; } $row = $table[$i]; // For each row, iterate first by lines within the row. for ($k = 0; $k < $heights[$i]; $k++) { // Add a vertical-bar at the beginning of each row line. $row_line = '|'; $trimmed = ''; // Within each row line, iterate by cells within that line. for ($j = 0; $j < $num_cols; $j++) { // Add a vertical bar at the end of each cell line. $row_line .= $row[$j][$k] . '|'; // chr(160) is the non-breaking space character. $trimmed .= trim($row[$j][$k], ' ' . $eol . chr(160)); } if ($trimmed > '') { // Only print rows that are non-empty. $text .= $row_line . $eol; } } } // Final output ends with a row separator. $text .= $separator; } } // Make sure formatted table content doesn't line-wrap. // chr(160) is the non-breaking space character. return str_replace(' ', chr(160), $text); } /** * Helper function for array_walk in drupal_wrap_mail(). * * Inserts $values['break'] sequences to break up $line into parts of no more * than $values['wrap'] characters. Only breaks at space characters, unless * $values['hard'] is TRUE. */ function _mailsystem_wrap_mail_line(&$line, $key, $values) { $line = wordwrap($line, $values['wrap'], $values['break'], $values['hard']); } /** * Helper function for array_walk in drupal_wrap_mail(). * * If $values['pad'] is non-empty, $values['indent'] will be added at the start * of each line, and $values['pad'] at the end, repeating the last character of * $values['pad'] until the line length equals $values['max']. * * If $values['pad'] is empty, $values['indent'] will be added at the start of * the first line, and $values['clean'] at the start of subsequent lines. * * If $values['stuff'] is true, then an extra space character will be added at * the start of any line beginning with a space, a '>', or the word 'From'. * * @see http://www.ietf.org/rfc/rfc3676.txt */ function _mailsystem_indent_mail_line(&$line, $key, $values) { if ($line == '') { return; } if ($values['pad']) { $line = $values['indent'] . $line; $count = $values['max'] - drupal_strlen($line) - drupal_strlen($values['pad']); if ($count >= 0) { $line .= $values['pad'] . str_repeat($values['pad_repeat'], $count); } } else { $line = $values[$key === 0 ? 'indent' : 'clean'] . $line; } if ($values['stuff']) { // chr(160) is the non-breaking space character. $line = preg_replace('/^(' . chr(160) . '| |>|From)/', ' $1', $line); } } /** * Helper function for drupal_wrap_mail() and drupal_html_to_text(). * * Replace all non-quotation markers from a given piece of indentation with * non-breaking space characters. */ function _mailsystem_html_to_text_clean($indent) { // chr(160) is the non-breaking space character. return preg_replace('/[^>]/', chr(160), $indent); }