]> git.mxchange.org Git - friendica.git/blob - library/html-to-markdown/HTML_To_Markdown.php
Merge remote-tracking branch 'upstream/develop' into 1502-improved-markdown
[friendica.git] / library / html-to-markdown / HTML_To_Markdown.php
1 <?php
2 /**
3  * Class HTML_To_Markdown
4  *
5  * A helper class to convert HTML to Markdown.
6  *
7  * @version 2.2.1
8  * @author Nick Cernis <nick@cern.is>
9  * @link https://github.com/nickcernis/html2markdown/ Latest version on GitHub.
10  * @link http://twitter.com/nickcernis Nick on twitter.
11  * @license http://www.opensource.org/licenses/mit-license.php MIT
12  */
13 class HTML_To_Markdown
14 {
15     /**
16      * @var DOMDocument The root of the document tree that holds our HTML.
17      */
18     private $document;
19
20     /**
21      * @var string|boolean The Markdown version of the original HTML, or false if conversion failed
22      */
23     private $output;
24
25     /**
26      * @var array Class-wide options users can override.
27      */
28     private $options = array(
29         'header_style'    => 'setext', // Set to "atx" to output H1 and H2 headers as # Header1 and ## Header2
30         'suppress_errors' => true, // Set to false to show warnings when loading malformed HTML
31         'strip_tags'      => false, // Set to true to strip tags that don't have markdown equivalents. N.B. Strips tags, not their content. Useful to clean MS Word HTML output.
32         'bold_style'      => '**', // Set to '__' if you prefer the underlined style
33         'italic_style'    => '*', // Set to '_' if you prefer the underlined style
34         'remove_nodes'    => '', // space-separated list of dom nodes that should be removed. example: "meta style script"
35     );
36
37
38     /**
39      * Constructor
40      *
41      * Set up a new DOMDocument from the supplied HTML, convert it to Markdown, and store it in $this->$output.
42      *
43      * @param string $html The HTML to convert to Markdown.
44      * @param array $overrides [optional] List of style and error display overrides.
45      */
46     public function __construct($html = null, $overrides = null)
47     {
48         if ($overrides)
49             $this->options = array_merge($this->options, $overrides);
50
51         if ($html)
52             $this->convert($html);
53     }
54
55
56     /**
57      * Setter for conversion options
58      *
59      * @param $name
60      * @param $value
61      */
62     public function set_option($name, $value)
63     {
64         $this->options[$name] = $value;
65     }
66
67
68     /**
69      * Convert
70      *
71      * Loads HTML and passes to get_markdown()
72      *
73      * @param $html
74      * @return string The Markdown version of the html
75      */
76     public function convert($html)
77     {
78         $html = preg_replace('~>\s+<~', '><', $html); // Strip white space between tags to prevent creation of empty #text nodes
79
80         $this->document = new DOMDocument();
81
82         if ($this->options['suppress_errors'])
83             libxml_use_internal_errors(true); // Suppress conversion errors (from http://bit.ly/pCCRSX )
84
85         $this->document->loadHTML('<?xml encoding="UTF-8">' . $html); // Hack to load utf-8 HTML (from http://bit.ly/pVDyCt )
86         $this->document->encoding = 'UTF-8';
87
88         if ($this->options['suppress_errors'])
89             libxml_clear_errors();
90
91         return $this->get_markdown($html);
92     }
93
94
95     /**
96      * Is Child Of?
97      *
98      * Is the node a child of the given parent tag?
99      *
100      * @param $parent_name string|array The name of the parent node(s) to search for e.g. 'code' or array('pre', 'code')
101      * @param $node
102      * @return bool
103      */
104     private static function is_child_of($parent_name, $node)
105     {
106         for ($p = $node->parentNode; $p != false; $p = $p->parentNode) {
107             if (is_null($p))
108                 return false;
109
110             if ( is_array($parent_name) && in_array($p->nodeName, $parent_name) )
111                 return true;
112             
113             if ($p->nodeName == $parent_name)
114                 return true;
115         }
116         return false;
117     }
118
119
120     /**
121      * Convert Children
122      *
123      * Recursive function to drill into the DOM and convert each node into Markdown from the inside out.
124      *
125      * Finds children of each node and convert those to #text nodes containing their Markdown equivalent,
126      * starting with the innermost element and working up to the outermost element.
127      *
128      * @param $node
129      */
130     private function convert_children($node)
131     {
132         // Don't convert HTML code inside <code> and <pre> blocks to Markdown - that should stay as HTML
133         if (self::is_child_of(array('pre', 'code'), $node))
134             return;
135
136         // If the node has children, convert those to Markdown first
137         if ($node->hasChildNodes()) {
138             $length = $node->childNodes->length;
139
140             for ($i = 0; $i < $length; $i++) {
141                 $child = $node->childNodes->item($i);
142                 $this->convert_children($child);
143             }
144         }
145
146         // Now that child nodes have been converted, convert the original node
147         $markdown = $this->convert_to_markdown($node);
148
149         // Create a DOM text node containing the Markdown equivalent of the original node
150         $markdown_node = $this->document->createTextNode($markdown);
151
152         // Replace the old $node e.g. "<h3>Title</h3>" with the new $markdown_node e.g. "### Title"
153         $node->parentNode->replaceChild($markdown_node, $node);
154     }
155
156
157     /**
158      * Get Markdown
159      *
160      * Sends the body node to convert_children() to change inner nodes to Markdown #text nodes, then saves and
161      * returns the resulting converted document as a string in Markdown format.
162      *
163      * @return string|boolean The converted HTML as Markdown, or false if conversion failed
164      */
165     private function get_markdown()
166     {
167         // Work on the entire DOM tree (including head and body)
168         $input = $this->document->getElementsByTagName("html")->item(0);
169
170         if (!$input)
171             return false;
172
173         // Convert all children of this root element. The DOMDocument stored in $this->doc will
174         // then consist of #text nodes, each containing a Markdown version of the original node
175         // that it replaced.
176         $this->convert_children($input);
177
178         // Sanitize and return the body contents as a string.
179         $markdown = $this->document->saveHTML(); // stores the DOMDocument as a string
180         $markdown = html_entity_decode($markdown, ENT_QUOTES, 'UTF-8');
181         $markdown = html_entity_decode($markdown, ENT_QUOTES, 'UTF-8'); // Double decode to cover cases like &amp;nbsp; http://www.php.net/manual/en/function.htmlentities.php#99984
182         $markdown = preg_replace("/<!DOCTYPE [^>]+>/", "", $markdown); // Strip doctype declaration
183         $unwanted = array('<html>', '</html>', '<body>', '</body>', '<head>', '</head>', '<?xml encoding="UTF-8">', '&#xD;');
184         $markdown = str_replace($unwanted, '', $markdown); // Strip unwanted tags
185         $markdown = trim($markdown, "\n\r\0\x0B");
186
187         $this->output = $markdown;
188
189         return $markdown;
190     }
191
192
193     /**
194      * Convert to Markdown
195      *
196      * Converts an individual node into a #text node containing a string of its Markdown equivalent.
197      *
198      * Example: An <h3> node with text content of "Title" becomes a text node with content of "### Title"
199      *
200      * @param $node
201      * @return string The converted HTML as Markdown
202      */
203     private function convert_to_markdown($node)
204     {
205         $tag = $node->nodeName; // the type of element, e.g. h1
206         $value = $node->nodeValue; // the value of that element, e.g. The Title
207         
208         // Strip nodes named in remove_nodes
209         $tags_to_remove = explode(' ', $this->options['remove_nodes']);
210         if ( in_array($tag, $tags_to_remove) )
211             return false;
212
213         switch ($tag) {
214             case "p":
215                 $markdown = (trim($value)) ? rtrim($value) . PHP_EOL . PHP_EOL : '';
216                 break;
217             case "pre":
218                 $markdown = PHP_EOL . $this->convert_code($node) . PHP_EOL;
219                 break;
220             case "h1":
221             case "h2":
222                 $markdown = $this->convert_header($tag, $node);
223                 break;
224             case "h3":
225                 $markdown = "### " . $value . PHP_EOL . PHP_EOL;
226                 break;
227             case "h4":
228                 $markdown = "#### " . $value . PHP_EOL . PHP_EOL;
229                 break;
230             case "h5":
231                 $markdown = "##### " . $value . PHP_EOL . PHP_EOL;
232                 break;
233             case "h6":
234                 $markdown = "###### " . $value . PHP_EOL . PHP_EOL;
235                 break;
236             case "em":
237             case "i":
238             case "strong":
239             case "b":
240                 $markdown = $this->convert_emphasis($tag, $value);
241                 break;
242             case "hr":
243                 $markdown = "- - - - - -" . PHP_EOL . PHP_EOL;
244                 break;
245             case "br":
246                 $markdown = "  " . PHP_EOL;
247                 break;
248             case "blockquote":
249                 $markdown = $this->convert_blockquote($node);
250                 break;
251             case "code":
252                 $markdown = $this->convert_code($node);
253                 break;
254             case "ol":
255             case "ul":
256                 $markdown = $value . PHP_EOL;
257                 break;
258             case "li":
259                 $markdown = $this->convert_list($node);
260                 break;
261             case "img":
262                 $markdown = $this->convert_image($node);
263                 break;
264             case "a":
265                 $markdown = $this->convert_anchor($node);
266                 break;
267             case "#text":
268                 $markdown = preg_replace('~\s+~', ' ', $value);
269                 $markdown = preg_replace('~^#~', '\\\\#', $markdown);
270                 break;
271             case "#comment":
272                 $markdown = '';
273                 break;
274             case "div":
275                 $markdown = ($this->options['strip_tags']) ? $value . PHP_EOL . PHP_EOL : html_entity_decode($node->C14N());
276                 break;
277             default:
278                 // If strip_tags is false (the default), preserve tags that don't have Markdown equivalents,
279                 // such as <span> nodes on their own. C14N() canonicalizes the node to a string.
280                 // See: http://www.php.net/manual/en/domnode.c14n.php
281                 $markdown = ($this->options['strip_tags']) ? $value : html_entity_decode($node->C14N());
282         }
283
284         return $markdown;
285     }
286
287
288     /**
289      * Convert Header
290      *
291      * Converts h1 and h2 headers to Markdown-style headers in setext style,
292      * matching the number of underscores with the length of the title.
293      *
294      * e.g.     Header 1    Header Two
295      *          ========    ----------
296      *
297      * Returns atx headers instead if $this->options['header_style'] is "atx"
298      *
299      * e.g.    # Header 1   ## Header Two
300      *
301      * @param string $level The header level, including the "h". e.g. h1
302      * @param string $node The node to convert.
303      * @return string The Markdown version of the header.
304      */
305     private function convert_header($level, $node)
306     {
307         $content = $node->nodeValue;
308
309         if (!$this->is_child_of('blockquote', $node) && $this->options['header_style'] == "setext") {
310             $length = (function_exists('mb_strlen')) ? mb_strlen($content, 'utf-8') : strlen($content);
311             $underline = ($level == "h1") ? "=" : "-";
312             $markdown = $content . PHP_EOL . str_repeat($underline, $length) . PHP_EOL . PHP_EOL; // setext style
313         } else {
314             $prefix = ($level == "h1") ? "# " : "## ";
315             $markdown = $prefix . $content . PHP_EOL . PHP_EOL; // atx style
316         }
317
318         return $markdown;
319     }
320
321
322     /**
323      * Converts inline styles
324      * This function is used to render strong and em tags
325      * 
326      * eg <strong>bold text</strong> becomes **bold text** or __bold text__
327      * 
328      * @param string $tag
329      * @param string $value
330      * @return string
331      */
332      private function convert_emphasis($tag, $value)
333      {
334         if ($tag == 'i' || $tag == 'em') {
335             $markdown = $this->options['italic_style'] . $value . $this->options['italic_style'];
336         } else {
337             $markdown = $this->options['bold_style'] . $value . $this->options['bold_style'];
338         }
339         
340         return $markdown;
341      }
342
343
344     /**
345      * Convert Image
346      *
347      * Converts <img /> tags to Markdown.
348      *
349      * e.g.     <img src="/path/img.jpg" alt="alt text" title="Title" />
350      * becomes  ![alt text](/path/img.jpg "Title")
351      *
352      * @param $node
353      * @return string
354      */
355     private function convert_image($node)
356     {
357         $src = $node->getAttribute('src');
358         $alt = $node->getAttribute('alt');
359         $title = $node->getAttribute('title');
360
361         if ($title != "") {
362             $markdown = '![' . $alt . '](' . $src . ' "' . $title . '")'; // No newlines added. <img> should be in a block-level element.
363         } else {
364             $markdown = '![' . $alt . '](' . $src . ')';
365         }
366
367         return $markdown;
368     }
369
370
371     /**
372      * Convert Anchor
373      *
374      * Converts <a> tags to Markdown.
375      *
376      * e.g.     <a href="http://modernnerd.net" title="Title">Modern Nerd</a>
377      * becomes  [Modern Nerd](http://modernnerd.net "Title")
378      *
379      * @param $node
380      * @return string
381      */
382     private function convert_anchor($node)
383     {
384         $href = $node->getAttribute('href');
385         $title = $node->getAttribute('title');
386         $text = $node->nodeValue;
387
388         if ($title != "") {
389             $markdown = '[' . $text . '](' . $href . ' "' . $title . '")';
390         } else {
391             $markdown = '[' . $text . '](' . $href . ')';
392         }
393
394         if (! $href)
395             $markdown = html_entity_decode($node->C14N());
396
397         // Append a space if the node after this one is also an anchor
398         $next_node_name = $this->get_next_node_name($node);
399
400         if ($next_node_name == 'a')
401             $markdown = $markdown . ' ';
402
403         return $markdown;
404     }
405
406
407     /**
408      * Convert List
409      *
410      * Converts <ul> and <ol> lists to Markdown.
411      *
412      * @param $node
413      * @return string
414      */
415     private function convert_list($node)
416     {
417         // If parent is an ol, use numbers, otherwise, use dashes
418         $list_type = $node->parentNode->nodeName;
419         $value = $node->nodeValue;
420
421         if ($list_type == "ul") {
422             $markdown = "- " . trim($value) . PHP_EOL;
423         } else {
424             $number = $this->get_position($node);
425             $markdown = $number . ". " . trim($value) . PHP_EOL;
426         }
427
428         return $markdown;
429     }
430
431
432     /**
433      * Convert Code
434      *
435      * Convert code tags by indenting blocks of code and wrapping single lines in backticks.
436      *
437      * @param DOMNode $node
438      * @return string
439      */
440     private function convert_code($node)
441     {
442         // Store the content of the code block in an array, one entry for each line
443
444         $markdown = '';
445
446         $code_content = html_entity_decode($node->C14N());
447         $code_content = str_replace(array("<code>", "</code>"), "", $code_content);
448         $code_content = str_replace(array("<pre>", "</pre>"), "", $code_content);
449
450         $lines = preg_split('/\r\n|\r|\n/', $code_content);
451         $total = count($lines);
452
453         // If there's more than one line of code, prepend each line with four spaces and no backticks.
454         if ($total > 1 || $node->nodeName === 'pre') {
455
456             // Remove the first and last line if they're empty
457             $first_line = trim($lines[0]);
458             $last_line = trim($lines[$total - 1]);
459             $first_line = trim($first_line, "&#xD;"); //trim XML style carriage returns too
460             $last_line = trim($last_line, "&#xD;");
461
462             if (empty($first_line))
463                 array_shift($lines);
464
465             if (empty($last_line))
466                 array_pop($lines);
467
468             $count = 1;
469             foreach ($lines as $line) {
470                 $line = str_replace('&#xD;', '', $line);
471                 $markdown .= "    " . $line;
472                 // Add newlines, except final line of the code
473                 if ($count != $total)
474                     $markdown .= PHP_EOL;
475                 $count++;
476             }
477             $markdown .= PHP_EOL;
478
479         } else { // There's only one line of code. It's a code span, not a block. Just wrap it with backticks.
480
481             $markdown .= "`" . $lines[0] . "`";
482
483         }
484         
485         return $markdown;
486     }
487
488
489     /**
490      * Convert blockquote
491      *
492      * Prepend blockquotes with > chars.
493      *
494      * @param $node
495      * @return string
496      */
497     private function convert_blockquote($node)
498     {
499         // Contents should have already been converted to Markdown by this point,
500         // so we just need to add ">" symbols to each line.
501
502         $markdown = '';
503
504         $quote_content = trim($node->nodeValue);
505
506         $lines = preg_split('/\r\n|\r|\n/', $quote_content);
507
508         $total_lines = count($lines);
509
510         foreach ($lines as $i => $line) {
511             $markdown .= "> " . $line . PHP_EOL;
512             if ($i + 1 == $total_lines)
513                 $markdown .= PHP_EOL;
514         }
515
516         return $markdown;
517     }
518
519
520     /**
521      * Get Position
522      *
523      * Returns the numbered position of a node inside its parent
524      *
525      * @param $node
526      * @return int The numbered position of the node, starting at 1.
527      */
528     private function get_position($node)
529     {
530         // Get all of the nodes inside the parent
531         $list_nodes = $node->parentNode->childNodes;
532         $total_nodes = $list_nodes->length;
533
534         $position = 1;
535
536         // Loop through all nodes and find the given $node
537         for ($a = 0; $a < $total_nodes; $a++) {
538             $current_node = $list_nodes->item($a);
539
540             if ($current_node->isSameNode($node))
541                 $position = $a + 1;
542         }
543
544         return $position;
545     }
546
547
548     /**
549      * Get Next Node Name
550      *
551      * Return the name of the node immediately after the passed one.
552      *
553      * @param $node
554      * @return string|null The node name (e.g. 'h1') or null.
555      */
556     private function get_next_node_name($node)
557     {
558         $next_node_name = null;
559
560         $current_position = $this->get_position($node);
561         $next_node = $node->parentNode->childNodes->item($current_position);
562
563         if ($next_node)
564             $next_node_name = $next_node->nodeName;
565
566         return $next_node_name;
567     }
568
569
570     /**
571      * To String
572      *
573      * Magic method to return Markdown output when HTML_To_Markdown instance is treated as a string.
574      *
575      * @return string
576      */
577     public function __toString()
578     {
579         return $this->output();
580     }
581
582
583     /**
584      * Output
585      *
586      * Getter for the converted Markdown contents stored in $this->output
587      *
588      * @return string
589      */
590     public function output()
591     {
592         if (!$this->output) {
593             return '';
594         } else {
595             return $this->output;
596         }
597     }
598 }