source)) { return $this->source; } $hash = md5($this->source); $cacheKey = sprintf('markdown_formatted_%s', $hash); if (Cache::has($cacheKey)) { return Cache::get($cacheKey); } // First, process the source with Blade to handle any dynamic content $result = Blade::render($this->source); // Convert markdown to HTML using the configured environment and extensions $converter = new MarkdownConverter($this->prepareEnvironment()); $converted = $converter->convert($result); // Perform final adjustments like inserting non-breaking spaces $result = $this->insertNonBreakingSpaces($converted->getContent()); $result = $this->removeEmptyCodeLines($result); Cache::put($cacheKey, $result, now()->addMonth()); return $result; } /** * Prepares the markdown parser environment with extensions and custom renderers. * * @return Environment The configured markdown environment. */ protected function prepareEnvironment(): Environment { // Load markdown configuration and initialize the environment $environment = new Environment(config('markdown')); // Define all the extensions to be used $extensions = [ new CommonMarkCoreExtension(), new HighlightCodeExtension(theme: 'aurora-x'), new FrontMatterExtension(), new FootnoteExtension(), new ExternalLinkExtension(), new HeadingPermalinkExtension(), new StrikethroughExtension(), new TableExtension(), new TableOfContentsExtension(), new DisallowedRawHtmlExtension(), new TaskListExtension(), ]; // Add each extension to the environment foreach ($extensions as $extension) { $environment->addExtension($extension); } // Use a custom renderer for links $environment->addRenderer(Link::class, new LinkRenderer()); return $environment; } /** * Inserts non-breaking spaces before certain punctuation marks in French typography. * * @param string $html The HTML content to process. * @return string The processed HTML content with non-breaking spaces added. */ protected function insertNonBreakingSpaces(string $html): string { // Define patterns for spaces that should be replaced with non-breaking spaces $patterns = [ '/ (\;)/', '/ (\:)/', '/ (\!)/', '/ (\?)/', ]; // Corresponding replacements for each pattern $replacements = array_fill(0, count($patterns), ' $1'); // Perform the replacements and return the modified HTML return preg_replace($patterns, $replacements, $html); } /** * For some reason, to date (04-2024), HighlightCodeExtension appends empty * lines at the end of a code block. We need to get rid of them. */ protected function removeEmptyCodeLines(string $html): string { $body = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'); libxml_use_internal_errors(true); $dom = new DOMDocument('1.0', 'UTF-8'); $dom->loadHtml($body); libxml_clear_errors(); $xpath = new \DOMXPath($dom); $spans = $xpath->query('//pre/code/span[@class="line" and not(node())]'); foreach ($spans as $span) { if ($span->isSameNode($span->parentNode->lastChild)) { $span->parentNode->removeChild($span); } } return $dom->saveHTML(); } }