PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.36.0
MailPoet – Newsletters, Email Marketing, and Automation v5.36.0
5.36.0 5.35.1 5.35.0 5.34.3 5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / vendor / woocommerce / email-editor / src / Engine / Renderer / class-html2text.php
mailpoet / vendor / woocommerce / email-editor / src / Engine / Renderer Last commit date
ContentRenderer 3 days ago class-html2text-exception.php 11 months ago class-html2text.php 11 months ago class-renderer.php 3 days ago index.php 11 months ago interface-css-inliner.php 11 months ago template-canvas.css 3 months ago template-canvas.php 3 months ago
class-html2text.php
438 lines
1 <?php
2 declare( strict_types = 1 );
3 namespace Automattic\WooCommerce\EmailEditor\Engine\Renderer;
4 if (!defined('ABSPATH')) exit;
5 class Html2Text {
6 public static function default_options(): array {
7 return array(
8 'ignore_errors' => false,
9 'drop_links' => false,
10 'char_set' => 'auto',
11 );
12 }
13 public static function convert( string $html, $options = array() ): string {
14 if ( false === $options || true === $options ) {
15 // Using old style (< 1.0) of passing in options.
16 $options = array( 'ignore_errors' => $options );
17 }
18 $options = array_merge( static::default_options(), $options );
19 // Check all options are valid.
20 foreach ( array_keys( $options ) as $key ) {
21 if ( ! in_array( $key, array_keys( static::default_options() ), true ) ) {
22 // Log invalid option for debugging purposes without exposing in exception.
23 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Security: Logging sensitive data separately from user-facing exception messages.
24 error_log( 'Html2Text: Invalid option provided: ' . htmlspecialchars( (string) $key, ENT_QUOTES, 'UTF-8' ) . '. Valid options are: ' . htmlspecialchars( implode( ',', array_keys( static::default_options() ) ), ENT_QUOTES, 'UTF-8' ) );
25 // Throw generic error message to avoid exposing user input.
26 throw new \InvalidArgumentException( 'Invalid option provided for html2text conversion.' );
27 }
28 }
29 $is_office_document = self::is_office_document( $html );
30 if ( $is_office_document ) {
31 // Remove office namespace.
32 $html = str_replace( array( '<o:p>', '</o:p>' ), '', $html );
33 }
34 $html = self::fix_newlines( $html );
35 // Use mb_convert_encoding for legacy versions of php.
36 if ( PHP_MAJOR_VERSION * 10 + PHP_MINOR_VERSION < 81 && mb_detect_encoding( $html, 'UTF-8', true ) ) {
37 $converted = mb_convert_encoding( $html, 'HTML-ENTITIES', 'UTF-8' );
38 $html = false !== $converted ? $converted : $html;
39 }
40 // Ensure $html is always a string before passing to get_document.
41 if ( ! is_string( $html ) ) {
42 $html = (string) $html;
43 }
44 $doc = self::get_document( $html, $options );
45 $output = self::iterate_over_node( $doc, null, false, $is_office_document, $options );
46 // Process output for whitespace/newlines.
47 $output = self::process_whitespace_newlines( $output );
48 return $output;
49 }
50 public static function fix_newlines( string $text ): string {
51 // Replace \r\n to \n.
52 $text = str_replace( "\r\n", "\n", $text );
53 // Remove \rs.
54 $text = str_replace( "\r", "\n", $text );
55 return $text;
56 }
57 public static function nbsp_codes(): array {
58 return array(
59 "\xc2\xa0",
60 "\u00a0",
61 );
62 }
63 public static function zwnj_codes(): array {
64 return array(
65 "\xe2\x80\x8c",
66 "\u200c",
67 );
68 }
69 public static function process_whitespace_newlines( string $text ): string {
70 // Remove excess spaces around tabs.
71 $result = preg_replace( '/ *\t */im', "\t", $text );
72 $text = null !== $result ? $result : $text;
73 // Remove leading whitespace.
74 $text = ltrim( $text );
75 // Remove leading spaces on each line.
76 $result = preg_replace( "/\n[ \t]*/im", "\n", $text );
77 $text = null !== $result ? $result : $text;
78 // Convert non-breaking spaces to regular spaces to prevent output issues,
79 // do it here so they do NOT get removed with other leading spaces, as they
80 // are sometimes used for indentation.
81 $text = self::render_text( $text );
82 // Remove trailing whitespace.
83 $text = rtrim( $text );
84 // Remove trailing spaces on each line.
85 $result = preg_replace( "/[ \t]*\n/im", "\n", $text );
86 $text = null !== $result ? $result : $text;
87 // Unarmor pre blocks.
88 $text = self::fix_newlines( $text );
89 // Remove unnecessary empty lines.
90 $result = preg_replace( "/\n\n\n*/im", "\n\n", $text );
91 return null !== $result ? $result : $text;
92 }
93 public static function is_office_document( string $html ): bool {
94 return strpos( $html, 'urn:schemas-microsoft-com:office' ) !== false;
95 }
96 public static function is_whitespace( string $text ): bool {
97 return 0 === strlen( trim( self::render_text( $text ), "\n\r\t " ) );
98 }
99 private static function get_document( string $html, array $options ): \DOMDocument {
100 $doc = new \DOMDocument();
101 $html = trim( $html );
102 if ( ! $html ) {
103 // DOMDocument doesn't support empty value and throws an error.
104 // Return empty document instead.
105 return $doc;
106 }
107 if ( '<' !== $html[0] ) {
108 // If HTML does not begin with a tag, we put a body tag around it.
109 // If we do not do this, PHP will insert a paragraph tag around
110 // the first block of text for some reason which can mess up
111 // the newlines. See pre.html test for an example.
112 $html = '<body>' . $html . '</body>';
113 }
114 $header = '';
115 // Use char sets for modern versions of php.
116 if ( PHP_MAJOR_VERSION * 10 + PHP_MINOR_VERSION >= 81 ) {
117 // Use specified char_set, or auto detect if not set.
118 $char_set = ! empty( $options['char_set'] ) && is_string( $options['char_set'] ) ? $options['char_set'] : 'auto';
119 if ( 'auto' === $char_set ) {
120 $detected = mb_detect_encoding( $html );
121 $char_set = false !== $detected ? $detected : 'UTF-8';
122 } elseif ( strpos( $char_set, ',' ) !== false ) {
123 $encoding_list = explode( ',', $char_set );
124 $encoding_list = array_map( 'trim', $encoding_list );
125 $encoding_list = array_filter(
126 $encoding_list,
127 function ( $encoding ) {
128 return ! empty( $encoding );
129 }
130 );
131 if ( ! empty( $encoding_list ) ) {
132 // Ensure we have a proper list with consecutive integer keys.
133 $encoding_list = array_values( $encoding_list );
134 mb_detect_order( $encoding_list );
135 $detected = mb_detect_encoding( $html );
136 $char_set = false !== $detected ? $detected : 'UTF-8';
137 }
138 }
139 // Turn off error detection for Windows-1252 legacy html.
140 if ( strpos( $char_set, '1252' ) !== false ) {
141 $options['ignore_errors'] = true;
142 }
143 $header = '<?xml version="1.0" encoding="' . $char_set . '">';
144 }
145 if ( ! empty( $options['ignore_errors'] ) ) {
146 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
147 $doc->strictErrorChecking = false;
148 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
149 $doc->recover = true;
150 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
151 $doc->xmlStandalone = true;
152 $old_internal_errors = libxml_use_internal_errors( true );
153 $load_result = $doc->loadHTML( $header . $html, LIBXML_NOWARNING | LIBXML_NOERROR | LIBXML_NONET | LIBXML_PARSEHUGE );
154 libxml_use_internal_errors( $old_internal_errors );
155 } else {
156 $load_result = $doc->loadHTML( $header . $html );
157 }
158 if ( ! $load_result ) {
159 // Log truncated HTML content for debugging purposes (limit to 500 chars to prevent log bloat).
160 $html_preview = strlen( $html ) > 500 ? substr( $html, 0, 500 ) . '...[truncated]' : $html;
161 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Security: Logging sensitive data separately from user-facing exception messages.
162 error_log( 'Html2Text: Failed to load HTML content: ' . htmlspecialchars( $html_preview, ENT_QUOTES, 'UTF-8' ) );
163 // Throw a generic error message to avoid exposing sensitive data.
164 throw new Html2Text_Exception( 'Could not load HTML - the content may be malformed.' );
165 }
166 return $doc;
167 }
168 private static function render_text( string $text ): string {
169 $text = str_replace( self::nbsp_codes(), ' ', $text );
170 $text = str_replace( self::zwnj_codes(), '', $text );
171 return $text;
172 }
173 private static function next_child_name( ?\DOMNode $node ): ?string {
174 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
175 if ( null === $node || null === $node->nextSibling ) {
176 return null;
177 }
178 // Get the next child.
179 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
180 $next_node = $node->nextSibling;
181 while ( null !== $next_node ) {
182 if ( $next_node instanceof \DOMText ) {
183 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
184 if ( ! self::is_whitespace( $next_node->wholeText ) ) {
185 break;
186 }
187 }
188 if ( $next_node instanceof \DOMElement ) {
189 break;
190 }
191 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
192 $next_node = $next_node->nextSibling;
193 }
194 $next_name = null;
195 if ( $next_node instanceof \DOMElement || $next_node instanceof \DOMText ) {
196 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
197 $next_name = strtolower( $next_node->nodeName );
198 }
199 return $next_name;
200 }
201 private static function iterate_over_node( \DOMNode $node, ?string $prev_name, bool $in_pre, bool $is_office_document, array $options ): string {
202 if ( $node instanceof \DOMText ) {
203 // Replace whitespace characters with a space (equivalent to \s).
204 if ( $in_pre ) {
205 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
206 $text = "\n" . trim( self::render_text( $node->wholeText ), "\n\r\t " ) . "\n";
207 // Remove trailing whitespace only.
208 $result = preg_replace( "/[ \t]*\n/im", "\n", $text );
209 $text = null !== $result ? $result : $text;
210 // Armor newlines with \r.
211 return str_replace( "\n", "\r", $text );
212 }
213 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
214 $text = self::render_text( $node->wholeText );
215 $result = preg_replace( "/[\\t\\n\\f\\r ]+/im", ' ', $text );
216 $text = null !== $result ? $result : $text;
217 if ( ! self::is_whitespace( $text ) && ( 'p' === $prev_name || 'div' === $prev_name ) ) {
218 return "\n" . $text;
219 }
220 return $text;
221 }
222 if ( $node instanceof \DOMDocumentType || $node instanceof \DOMProcessingInstruction ) {
223 // Ignore.
224 return '';
225 }
226 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
227 $name = strtolower( $node->nodeName );
228 $next_name = self::next_child_name( $node );
229 // Start whitespace.
230 switch ( $name ) {
231 case 'hr':
232 $prefix = '';
233 if ( null !== $prev_name ) {
234 $prefix = "\n";
235 }
236 return $prefix . "---------------------------------------------------------------\n";
237 case 'style':
238 case 'head':
239 case 'title':
240 case 'meta':
241 case 'script':
242 // Ignore these tags.
243 return '';
244 case 'h1':
245 case 'h2':
246 case 'h3':
247 case 'h4':
248 case 'h5':
249 case 'h6':
250 case 'ol':
251 case 'ul':
252 case 'pre':
253 // Add two newlines.
254 $output = "\n\n";
255 break;
256 case 'td':
257 case 'th':
258 // Add tab char to separate table fields.
259 $output = "\t";
260 break;
261 case 'p':
262 // Microsoft exchange emails often include HTML which, when passed through
263 // html2text, results in lots of double line returns everywhere.
264 //
265 // To fix this, for any p element with a className of `MsoNormal` (the standard
266 // classname in any Microsoft export or outlook for a paragraph that behaves
267 // like a line return) we skip the first line returns and set the name to br.
268 if ( $is_office_document && $node instanceof \DOMElement && 'MsoNormal' === $node->getAttribute( 'class' ) ) {
269 $output = '';
270 $name = 'br';
271 break;
272 }
273 // Add two lines.
274 $output = "\n\n";
275 break;
276 case 'tr':
277 // Add one line.
278 $output = "\n";
279 break;
280 case 'div':
281 $output = '';
282 if ( null !== $prev_name ) {
283 // Add one line.
284 $output .= "\n";
285 }
286 break;
287 case 'li':
288 $output = '- ';
289 break;
290 default:
291 // Print out contents of unknown tags.
292 $output = '';
293 break;
294 }
295 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
296 if ( $node->childNodes->length > 0 ) {
297 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
298 $n = $node->childNodes->item( 0 );
299 $previous_sibling_names = array();
300 $previous_sibling_name = null;
301 $parts = array();
302 $trailing_whitespace = 0;
303 while ( null !== $n ) {
304 $text = self::iterate_over_node( $n, $previous_sibling_name, $in_pre || 'pre' === $name, $is_office_document, $options );
305 // Pass current node name to next child, as previousSibling does not appear to get populated.
306 if ( $n instanceof \DOMDocumentType
307 || $n instanceof \DOMProcessingInstruction
308 || ( $n instanceof \DOMText && self::is_whitespace( $text ) ) ) {
309 // Keep current previousSiblingName, these are invisible.
310 ++$trailing_whitespace;
311 } else {
312 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
313 $previous_sibling_name = strtolower( $n->nodeName );
314 $previous_sibling_names[] = $previous_sibling_name;
315 $trailing_whitespace = 0;
316 }
317 $node->removeChild( $n );
318 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
319 $n = $node->childNodes->item( 0 );
320 $parts[] = $text;
321 }
322 // Remove trailing whitespace, important for the br check below.
323 while ( $trailing_whitespace-- > 0 ) {
324 array_pop( $parts );
325 }
326 // Suppress last br tag inside a node list if follows text.
327 $last_name = array_pop( $previous_sibling_names );
328 if ( 'br' === $last_name ) {
329 $last_name = array_pop( $previous_sibling_names );
330 if ( '#text' === $last_name ) {
331 array_pop( $parts );
332 }
333 }
334 $output .= implode( '', $parts );
335 }
336 // End whitespace.
337 switch ( $name ) {
338 case 'h1':
339 case 'h2':
340 case 'h3':
341 case 'h4':
342 case 'h5':
343 case 'h6':
344 case 'pre':
345 case 'p':
346 // Add two lines.
347 $output .= "\n\n";
348 break;
349 case 'br':
350 // Add one line.
351 $output .= "\n";
352 break;
353 case 'div':
354 break;
355 case 'a':
356 // Links are returned in [text](link) format.
357 $href = $node instanceof \DOMElement ? $node->getAttribute( 'href' ) : '';
358 $output = trim( $output );
359 // Remove double [[ ]] s from linking images.
360 if ( '[' === substr( $output, 0, 1 ) && ']' === substr( $output, -1 ) ) {
361 $output = substr( $output, 1, strlen( $output ) - 2 );
362 // For linking images, the title of the <a> overrides the title of the <img>.
363 if ( $node instanceof \DOMElement && $node->getAttribute( 'title' ) ) {
364 $output = $node->getAttribute( 'title' );
365 }
366 }
367 // If there is no link text, but a title attr.
368 if ( ! $output && $node instanceof \DOMElement && $node->getAttribute( 'title' ) ) {
369 $output = $node->getAttribute( 'title' );
370 }
371 if ( ! $href ) {
372 // It doesn't link anywhere.
373 if ( $node instanceof \DOMElement && $node->getAttribute( 'name' ) ) {
374 if ( $options['drop_links'] ) {
375 $output = "$output";
376 } else {
377 $output = "[$output]";
378 }
379 }
380 } elseif ( $href === $output || "mailto:$output" === $href || "http://$output" === $href || "https://$output" === $href ) {
381 // Link to the same address: just use link.
382 $output = "$output";
383 } elseif ( $output ) {
384 // Replace it.
385 if ( $options['drop_links'] ) {
386 $output = "$output";
387 } else {
388 $output = "[$output]($href)";
389 }
390 } else {
391 // Empty string.
392 $output = "$href";
393 }
394 // Does the next node require additional whitespace?
395 switch ( $next_name ) {
396 case 'h1':
397 case 'h2':
398 case 'h3':
399 case 'h4':
400 case 'h5':
401 case 'h6':
402 $output .= "\n";
403 break;
404 }
405 break;
406 case 'img':
407 if ( $node instanceof \DOMElement && $node->getAttribute( 'title' ) ) {
408 $output = '[' . $node->getAttribute( 'title' ) . ']';
409 } elseif ( $node instanceof \DOMElement && $node->getAttribute( 'alt' ) ) {
410 $output = '[' . $node->getAttribute( 'alt' ) . ']';
411 } else {
412 $output = '';
413 }
414 break;
415 case 'li':
416 $output .= "\n";
417 break;
418 case 'blockquote':
419 // Process quoted text for whitespace/newlines.
420 $output = self::process_whitespace_newlines( $output );
421 // Add leading newline.
422 $output = "\n" . $output;
423 // Prepend '> ' at the beginning of all lines.
424 $result = preg_replace( "/\n/im", "\n> ", $output );
425 $output = null !== $result ? $result : $output;
426 // Replace leading '> >' with '>>'.
427 $result = preg_replace( "/\n> >/im", "\n>>", $output );
428 $output = null !== $result ? $result : $output;
429 // Add another leading newline and trailing newlines.
430 $output = "\n" . $output . "\n\n";
431 break;
432 default:
433 // Do nothing.
434 }
435 return $output;
436 }
437 }
438