move formatting from JS to PHP
[mdref/mdref] / mdref / Formatter.php
index ecde240b5ce588c81a94b8024e1e768167b3ae2f..95a276f0514737574a83a2893f2d1f3bfaf36058 100644 (file)
 
 namespace mdref;
 
-use function class_exists;
+use DOMDocument;
+use DOMElement;
+use DOMNode;
+use League\CommonMark\GithubFlavoredMarkdownConverter;
+use League\CommonMark\MarkdownConverter;
+use League\CommonMark\Normalizer;
+use League\CommonMark\Extension;
+use mdref\Formatter\Wrapper;
 
-abstract class Formatter {
-    abstract function formatString(string $string) : string;
-    abstract function formatFile(string $file) : string;
+class Formatter {
+       public function __construct(
+               protected ?MarkdownConverter $md = null,
+               protected ?Wrapper $wrapper = null,
+       ) {
+               if (!$this->md) {
+                       $this->md = new GithubFlavoredMarkdownConverter([
+                               "slug_normalizer" => [
+                                       "instance" => new class($this) implements Normalizer\TextNormalizerInterface {
+                                               protected $formatter;
+                                               function __construct(Formatter $fmt) {
+                                                       $this->formatter = $fmt;
+                                               }
+                                               function normalize(string $text, $context = null) : string {
+                                                       return $this->formatter->formatSlug($text);
+                                               }
+                                       }
+                               ],
+                       ]);
+                       $this->md->getEnvironment()->addExtension(
+                               new Extension\DescriptionList\DescriptionListExtension
+                       );
+                       $this->md->getEnvironment()->addExtension(
+                               new Extension\Attributes\AttributesExtension
+                       );
+               }
+               if (!$this->wrapper) {
+                       $this->wrapper = new Wrapper($this);
+               }
+       }
+
+       public function formatString(string $string) : string {
+               return $this->md->convertToHtml($string);
+       }
+
+       public function formatFile(string $file) : string {
+               $string = file_get_contents($file);
+               if ($string === false) {
+                       throw Exception::fromLastError();
+               }
+               return $this->md->convertToHtml($string);
+       }
+
+       /**
+        * Format a simplified url slug
+        * @param string $string input text, like a heading
+        * @return string the simplified slug
+        */
+       public function formatSlug(string $string) : string {
+               return preg_replace("/[^\$[:alnum:]:._-]+/", ".", $string);
+       }
+
+       /**
+        * @param string $page HTML content
+        * @param object $pld Action payload
+        * @return string marked up HTML content
+        */
+       public function markup(string $page, $pld) : string {
+               $dom = new DOMDocument("1.0", "utf-8");
+               $dom->formatOutput = true;
+               $dom->loadHTML("<!doctype html>\n <meta charset=utf-8>\n" . $page, LIBXML_HTML_NOIMPLIED);
+               foreach ($dom->childNodes as $node) {
+                       $this->walk($node, $pld);
+               }
+               $html = "";
+               foreach ($dom->childNodes as $child) {
+                       $html .= $dom->saveHTML($child);
+               }
+               return $html;
+       }
+
+       public function createPermaLink(DOMElement $node, string $slug, $pld) {
+               if (strlen($slug)) {
+                       $node->setAttribute("id", $slug);
+               }
+               $perm = $node->ownerDocument->createElement("a");
+               $perm->setAttribute("class", "permalink");
+               $perm->setAttribute("href", "$pld->ref#$slug");
+               $perm->textContent = "#";
+               return $perm;
+       }
+
+       protected function walk(DOMNode $node, $pld) {
+               switch ($node->nodeType) {
+                       case XML_ELEMENT_NODE:
+                               $this->walkElement($node, $pld);
+                               break;
+                       case XML_TEXT_NODE:
+                               $this->wrapper->wrap($node, $pld);
+                               break;
+                       default:
+                               break;
+               }
+       }
+
+       protected function highlightCode(DOMElement $node) {
+               foreach (["default", "comment", "html", "keyword", "string"] as $type) {
+                       ini_set("highlight.$type", "inherit\" class=\"$type");
+               }
+               $code = highlight_string($node->textContent, true);
+               $temp = new DOMDocument("1.0", "utf-8");
+               $temp->loadHTML($code, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
+               return $node->ownerDocument->importNode($temp->firstChild, true);
+       }
 
-       static function factory() : Formatter {
-               if (class_exists("League\\CommonMark\\GithubFlavoredMarkdownConverter", true)) {
-                       return new Formatter\League;
+       protected function walkElement(DOMElement $node, $pld) {
+               switch ($node->tagName) {
+                       case "h1":
+                               $perm = $this->createPermaLink($node, "", $pld);
+                               $node->insertBefore($perm, $node->firstChild);
+                               $pld->currentSection = null;
+                               break;
+                       case "h2":
+                               $pld->currentSection = $this->formatSlug($node->textContent);
+                       case "h3":
+                       case "h4":
+                       case "h5":
+                       case "h6":
+                               $slug = $this->formatSlug($node->textContent);
+                               $perm = $this->createPermaLink($node, $slug, $pld);
+                               $node->appendChild($perm);
+                               break;
+                       case "span":
+                               if (!empty($pld->currentSection) && $node->hasAttribute("class")) {
+                                       switch ($pld->currentSection) {
+                                               case "Properties:":
+                                               case "Constants:":
+                                                       switch ($node->getAttribute("class")) {
+                                                               case "constant":
+                                                               case "var":
+                                                                       $slug = $this->formatSlug($node->textContent);
+                                                                       $perm = $this->createPermaLink($node, $slug, $pld);
+                                                                       $node->insertBefore($perm);
+                                                                       break;
+                                                       }
+                                                       break;
+                                       }
+                               }
+                       case "a":
+                       case "br":
+                       case "hr":
+                       case "em":
+                               return; // !
+                       case "code":
+                               if ($node->parentNode && $node->parentNode->nodeName === "pre") {
+                                       $code = $this->highlightCode($node);
+                                       $this->walk($code, $pld);
+                                       $node->parentNode->replaceChild($code, $node);
+                               }
+                               return; // !
                }
-               if (extension_loaded("discount")) {
-                       return new Formatter\Discount;
+
+               // suck it out, because we're modifying the DOM
+               foreach (iterator_to_array($node->childNodes) as $child) {
+                       $this->walk($child, $pld);
                }
-               throw new \Exception("No Markdown implementation found");
        }
 }