From 2ced025ccc01c01263335819a9b1e0497f366323 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Mon, 15 Jul 2024 15:14:36 +1000 Subject: [PATCH] Render blockquotes on the server --- app/Markdown/BlockQuoteRenderer.php | 92 +++++++++++++++++++ .../GithubFlavoredMarkdownConverter.php | 18 +++- .../GithubFlavoredMarkdownExtension.php | 21 ----- public/img/callouts/star.min.svg | 1 - .../images/exclamation.svg | 2 +- .../images/laracast.svg | 0 .../images/lightbulb.svg | 0 resources/js/docs.js | 84 ----------------- resources/views/docs.blade.php | 8 +- tailwind.config.js | 1 + 10 files changed, 117 insertions(+), 110 deletions(-) create mode 100644 app/Markdown/BlockQuoteRenderer.php delete mode 100644 app/Markdown/GithubFlavoredMarkdownExtension.php delete mode 100644 public/img/callouts/star.min.svg rename public/img/callouts/exclamation.min.svg => resources/images/exclamation.svg (68%) rename public/img/callouts/laracast.min.svg => resources/images/laracast.svg (100%) rename public/img/callouts/lightbulb.min.svg => resources/images/lightbulb.svg (100%) diff --git a/app/Markdown/BlockQuoteRenderer.php b/app/Markdown/BlockQuoteRenderer.php new file mode 100644 index 00000000..d27099f8 --- /dev/null +++ b/app/Markdown/BlockQuoteRenderer.php @@ -0,0 +1,92 @@ +render($node, $childRenderer); + + if (! $element instanceof HtmlElement || $element->getTagName() !== 'blockquote') { + return $element; + } + + $html = trim($element->getContents(true)); + + if (! str_starts_with($html, '

')) { + return $element; + } + + $htmlPartial = Str::after($html, '

'); + + if (preg_match('/^\[!(NOTE|WARNING)\](?:
)?/', $htmlPartial, $matches)) { + // GitHub styled notes, e.g., + // > [!NOTE] Content + // or + // > [!NOTE] + // > Content + } elseif (preg_match('/^(Note|Warning):?<\/strong>:?(?:
)?/', $htmlPartial, $matches)) { + // Legacy GitHub styled notes, e.g., + // > **Note:** Content + // or + // > **Note**: Content + // or + // > **Note** Content + // or + // > **Note:** + // > Content + // or + // > **Note**: + // > Content + // or + // > **Note** + // > Content + } elseif (preg_match('/^\{(note|tip|video)\}/', $htmlPartial, $matches)) { + // Legacy Laravel styled notes, e.g., + // > {tip} Content + // or + // > {tip} + // > Content + } else { + return $element; + } + + $bits = match ($matches[1]) { + 'WARNING', 'Warning', 'note' => [Vite::content('resources/images/exclamation.svg'), 'bg-red-600'], + 'NOTE', 'Note', 'tip' => [Vite::content('resources/images/lightbulb.svg'), 'bg-purple-600'], + 'video' => [Vite::content('resources/images/laracast.svg'), 'bg-blue-600'], + default => null, + }; + + if ($bits === null) { + return $element; + } + + [$asset, $class] = $bits; + + $bodyPartial = str_replace($matches[0], '', $htmlPartial); + + return << +

+
{$asset}
+
+

{$bodyPartial} + + HTML; + } +} diff --git a/app/Markdown/GithubFlavoredMarkdownConverter.php b/app/Markdown/GithubFlavoredMarkdownConverter.php index f464dc5e..77aafca7 100644 --- a/app/Markdown/GithubFlavoredMarkdownConverter.php +++ b/app/Markdown/GithubFlavoredMarkdownConverter.php @@ -2,14 +2,25 @@ namespace App\Markdown; +use Illuminate\Support\Str; +use League\CommonMark\Extension\Autolink\AutolinkExtension; +use League\CommonMark\Extension\Strikethrough\StrikethroughExtension; +use League\CommonMark\Extension\Table\TableExtension; +use League\CommonMark\Extension\TaskList\TaskListExtension; use League\CommonMark\MarkdownConverter; use League\CommonMark\Environment\Environment; use App\Markdown\GithubFlavoredMarkdownExtension; +use Illuminate\Support\Facades\Vite; +use League\CommonMark\Node\Node; +use League\CommonMark\Renderer\ChildNodeRendererInterface; +use League\CommonMark\Util\HtmlElement; use Torchlight\Commonmark\V2\TorchlightExtension; use Laravel\Unfenced\UnfencedExtension; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Extension\Attributes\AttributesExtension; use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; +use League\CommonMark\Extension\CommonMark\Node\Block\BlockQuote; +use League\CommonMark\Renderer\NodeRendererInterface; /** * Converts GitHub Flavored Markdown to HTML. @@ -25,11 +36,16 @@ public function __construct(array $config = []) { $environment = new Environment($config); $environment->addExtension(new CommonMarkCoreExtension()); - $environment->addExtension(new GithubFlavoredMarkdownExtension()); + $environment->addExtension(new AutolinkExtension()); + $environment->addExtension(new StrikethroughExtension()); + $environment->addExtension(new TableExtension()); + $environment->addExtension(new TaskListExtension()); $environment->addExtension(new UnfencedExtension()); $environment->addExtension(new AttributesExtension()); $environment->addExtension(new TorchlightExtension()); + $environment->addRenderer(BlockQuote::class, new BlockQuoteRenderer()); + parent::__construct($environment); } diff --git a/app/Markdown/GithubFlavoredMarkdownExtension.php b/app/Markdown/GithubFlavoredMarkdownExtension.php deleted file mode 100644 index 7a4d8c6f..00000000 --- a/app/Markdown/GithubFlavoredMarkdownExtension.php +++ /dev/null @@ -1,21 +0,0 @@ -addExtension(new AutolinkExtension()); - $environment->addExtension(new StrikethroughExtension()); - $environment->addExtension(new TableExtension()); - $environment->addExtension(new TaskListExtension()); - } -} diff --git a/public/img/callouts/star.min.svg b/public/img/callouts/star.min.svg deleted file mode 100644 index dc7524ab..00000000 --- a/public/img/callouts/star.min.svg +++ /dev/null @@ -1 +0,0 @@ -star \ No newline at end of file diff --git a/public/img/callouts/exclamation.min.svg b/resources/images/exclamation.svg similarity index 68% rename from public/img/callouts/exclamation.min.svg rename to resources/images/exclamation.svg index b9ce3950..c0d4398e 100644 --- a/public/img/callouts/exclamation.min.svg +++ b/resources/images/exclamation.svg @@ -1 +1 @@ -exclamation \ No newline at end of file +exclamation diff --git a/public/img/callouts/laracast.min.svg b/resources/images/laracast.svg similarity index 100% rename from public/img/callouts/laracast.min.svg rename to resources/images/laracast.svg diff --git a/public/img/callouts/lightbulb.min.svg b/resources/images/lightbulb.svg similarity index 100% rename from public/img/callouts/lightbulb.min.svg rename to resources/images/lightbulb.svg diff --git a/resources/js/docs.js b/resources/js/docs.js index 16c06796..1ecc2fdc 100644 --- a/resources/js/docs.js +++ b/resources/js/docs.js @@ -4,7 +4,6 @@ import './theme' document.addEventListener('DOMContentLoaded', () => { wrapHeadingsInAnchors(); setupNavCurrentLinkHandling(); - replaceBlockquotesWithCalloutsInDocs(); highlightSupportPolicyTable(); const skipToContentLink = document.querySelector('#skip-to-content-link'); @@ -48,89 +47,6 @@ function setupNavCurrentLinkHandling() { }); } -function replaceBlockquotesWithCalloutsInDocs() { - [...document.querySelectorAll('.docs_main blockquote p')].forEach(el => { - // Legacy Laravel styled notes... - replaceBlockquote(el, /\{(.*?)\}/, (type) => { - switch (type) { - case "note": - return ['/img/callouts/exclamation.min.svg', 'bg-red-600', 6, 35]; - case "tip": - return ['/img/callouts/lightbulb.min.svg', 'bg-purple-600', 28, 40]; - case "laracasts": - case "video": - return ['/img/callouts/laracast.min.svg', 'bg-blue-600', 49, 40]; - default: - return [null, null, 0, 0]; - } - }); - - // GitHub styled notes... - replaceBlockquote(el, /^\[\!(.*?)\](?:
\n?)?/, (type) => { - switch (type) { - case "WARNING": - return ['/img/callouts/exclamation.min.svg', 'bg-red-600', 6, 35]; - case "NOTE": - return ['/img/callouts/lightbulb.min.svg', 'bg-purple-600', 28, 40]; - default: - return [null, null, 0, 0]; - } - }); - - // Legagcy GitHub styled notes... - replaceBlockquote(el, /^(.*?)<\/strong>(?:
\n?)?/, (type) => { - switch (type) { - case "Warning": - return ['/img/callouts/exclamation.min.svg', 'bg-red-600', 6, 35]; - case "Note": - return ['/img/callouts/lightbulb.min.svg', 'bg-purple-600', 28, 40]; - default: - return [null, null, 0, 0]; - } - }); - }); -} - -function replaceBlockquote(el, regex, getImageAndColorByType) { - var str = el.innerHTML; - var match = str.match(regex); - var img, color, width, height; - - if (match) { - var type = match[1] || false; - } - - if (type) { - [img, color, width, height] = getImageAndColorByType(type); - - if (img === null && color === null) { - return; - } - - const wrapper = document.createElement('div'); - wrapper.classList = 'mb-10 max-w-2xl mx-auto px-4 py-8 shadow-lg lg:flex lg:items-center'; - - const imageWrapper = document.createElement('div'); - imageWrapper.classList = `w-20 h-20 mb-6 flex items-center justify-center shrink-0 ${color} lg:mb-0`; - const image = document.createElement('img'); - image.src = img; - image.height = height - image.width = width - image.loading = 'lazy' - image.classList = `opacity-75`; - imageWrapper.appendChild(image); - wrapper.appendChild(imageWrapper); - - el.parentNode.insertBefore(wrapper, el); - - el.innerHTML = str.replace(regex, ''); - - el.classList = 'mb-0 lg:ml-6'; - wrapper.classList.add('callout'); - wrapper.appendChild(el); - } -} - function highlightSupportPolicyTable() { function highlightCells(table) { diff --git a/resources/views/docs.blade.php b/resources/views/docs.blade.php index 8d2e8b44..6337de33 100644 --- a/resources/views/docs.blade.php +++ b/resources/views/docs.blade.php @@ -192,7 +192,9 @@ class="appearance-none flex-1 w-full px-0 py-1 placeholder-gray-900 tracking-wid

- Icon +
+ {!! Vite::content('resources/images/exclamation.svg') !!} +

@@ -209,7 +211,9 @@ class="appearance-none flex-1 w-full px-0 py-1 placeholder-gray-900 tracking-wid

- Icon +
+ {!! Vite::content('resources/images/exclamation.svg') !!} +

diff --git a/tailwind.config.js b/tailwind.config.js index ad72fcc5..7031be30 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -37,6 +37,7 @@ const accentColors = { export default { content: [ + 'app/Markdown/*.php', 'resources/views/**/*.blade.php', 'resources/js/**/*.js', ],