From d403675d9abc8afa97c6a499b7ce4cb41b115c2a Mon Sep 17 00:00:00 2001 From: sloane <git@sloanelybutsurely.com> Date: Wed, 16 Apr 2025 08:08:22 -0400 Subject: [PATCH] remove custom markdown document -> html, tailwind prose --- lib/web/components/core_components.ex | 2 +- lib/web/controllers/page_html/home.html.heex | 4 +- lib/web/markdown.ex | 325 +------------------ 3 files changed, 14 insertions(+), 317 deletions(-) diff --git a/lib/web/components/core_components.ex b/lib/web/components/core_components.ex index 5c2d348..43cf78e 100644 --- a/lib/web/components/core_components.ex +++ b/lib/web/components/core_components.ex @@ -167,7 +167,7 @@ defmodule Web.CoreComponents do def markdown(assigns) do ~H""" - <div class={["markdown-content", @class]}> + <div class={["prose", @class]}> {Phoenix.HTML.raw(Web.Markdown.to_html(@content))} </div> """ diff --git a/lib/web/controllers/page_html/home.html.heex b/lib/web/controllers/page_html/home.html.heex index 12e1361..dda1b4c 100644 --- a/lib/web/controllers/page_html/home.html.heex +++ b/lib/web/controllers/page_html/home.html.heex @@ -16,8 +16,8 @@ navigate={Web.Paths.public_blog_path(blog)} class="flex justify-between items-center hover:bg-gray-50 -mx-2 px-2 py-1 rounded" > - <h3 class="p-name u-url">{blog.title}</h3> - <div class="text-gray-500 dt-published"> + <h3 class="p-name u-url truncate">{blog.title}</h3> + <div class="text-gray-500 dt-published flex-shrink-0"> <.timex value={Core.Posts.publish_date_time(blog)} format="{Mfull} {D}, {YYYY}" /> </div> </.link> diff --git a/lib/web/markdown.ex b/lib/web/markdown.ex index 784aa66..0ac9866 100644 --- a/lib/web/markdown.ex +++ b/lib/web/markdown.ex @@ -1,323 +1,20 @@ defmodule Web.Markdown do @moduledoc """ - Converts markdown to HTML using MDEx, with custom rendering to add Tailwind CSS classes. - Uses iodata for efficient string generation. + Converts markdown to HTML using MDEx. """ - alias MDEx.Document - - @doc """ - Converts markdown to HTML with syntax highlighting, additional extensions, - and appropriate Tailwind CSS classes for styling. - """ def to_html(markdown) when is_binary(markdown) do - # First parse the document to get the AST - {:ok, document} = - MDEx.parse_document(markdown, - extension: [ - strikethrough: true, - table: true, - autolink: true, - tasklist: true, - footnotes: true - ], - features: [syntax_highlight_theme: "catppuccin_latte"] - ) - - # Render the document to HTML with Tailwind classes - render_document(document) + MDEx.to_html!(markdown, + extension: [ + strikethrough: true, + table: true, + autolink: true, + tasklist: true, + footnotes: true + ], + features: [syntax_highlight_theme: "catppuccin_latte"] + ) end def to_html(nil), do: "" - - # Core rendering function for the document - defp render_document(%Document{} = document) do - document.nodes - |> Enum.map(&render_node(&1, nil)) - end - - # Heading (h1-h6) with anchor links - defp render_node(%MDEx.Heading{level: level} = node, _parent) do - tag = "h#{level}" - - class = - case level do - 1 -> "text-3xl font-bold mt-6 mb-4 group" - 2 -> "text-2xl font-bold mt-5 mb-3 group" - 3 -> "text-xl font-bold mt-4 mb-2 group" - 4 -> "text-lg font-bold mt-3 mb-2 group" - 5 -> "text-base font-bold mt-3 mb-2 group" - 6 -> "text-sm font-bold mt-3 mb-2 group" - end - - # Generate a slug from the text content for the ID - content = Enum.map(node.nodes, &render_node(&1, node)) - content_text = extract_text_content(node) - slug = Slug.slugify(content_text) - - [ - "<", - tag, - " id=\"", - slug, - "\" class=\"", - class, - " relative\">", - "<a href=\"#", - slug, - "\" class=\"absolute -left-5 text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity\">¶</a>", - content, - "</", - tag, - ">" - ] - end - - # Paragraph - defp render_node(%MDEx.Paragraph{} = node, _parent) do - content = Enum.map(node.nodes, &render_node(&1, node)) - ["<p>", content, "</p>"] - end - - # Text - defp render_node(%MDEx.Text{literal: literal}, _parent) do - literal - end - - # Strong (bold) - defp render_node(%MDEx.Strong{} = node, _parent) do - content = Enum.map(node.nodes, &render_node(&1, node)) - ["<strong>", content, "</strong>"] - end - - # Emphasis (italic) - defp render_node(%MDEx.Emph{} = node, _parent) do - content = Enum.map(node.nodes, &render_node(&1, node)) - ["<em>", content, "</em>"] - end - - # Strikethrough - defp render_node(%MDEx.Strikethrough{} = node, _parent) do - content = Enum.map(node.nodes, &render_node(&1, node)) - ["<del>", content, "</del>"] - end - - # Link - defp render_node(%MDEx.Link{url: url, title: title} = node, _parent) do - content = Enum.map(node.nodes, &render_node(&1, node)) - title_attr = if title, do: [" title=\"", title, "\""], else: [] - - [ - "<a href=\"", - url, - "\"", - title_attr, - " class=\"text-blue-600 hover:text-blue-800 hover:underline\">", - content, - "</a>" - ] - end - - # Image - defp render_node(%MDEx.Image{url: url, title: title} = node, _parent) do - alt = Enum.map(node.nodes, &render_node(&1, node)) - title_attr = if title, do: [" title=\"", title, "\""], else: [] - - [ - "<img src=\"", - url, - "\" alt=\"", - alt, - "\"", - title_attr, - " class=\"max-w-full my-4 rounded\">" - ] - end - - # Code (inline) - defp render_node(%MDEx.Code{literal: literal}, _parent) do - ["<code class=\"bg-gray-100 rounded px-1 py-0.5 text-sm\">", literal, "</code>"] - end - - # Code blocks - use MDEx's built-in HTML rendering to preserve syntax highlighting - defp render_node(%MDEx.CodeBlock{} = node, _parent) do - # Use MDEx's built-in HTML rendering for code blocks to preserve syntax highlighting - {:ok, html} = - MDEx.to_html( - %MDEx.Document{nodes: [node]}, - extension: [], - features: [syntax_highlight_theme: "catppuccin_latte"] - ) - - # Return the HTML as is since it's already properly formatted with syntax highlighting - String.replace(html, ~s|class="autumn-hl"|, ~s|class="autumn-hl p-2 rounded"|) - end - - # Lists - Bullet - defp render_node(%MDEx.List{list_type: :bullet} = node, _parent) do - content = Enum.map(node.nodes, &render_node(&1, node)) - ["<ul class=\"list-disc pl-6 my-4\">", content, "</ul>"] - end - - # Lists - Ordered - defp render_node(%MDEx.List{list_type: :ordered} = node, _parent) do - content = Enum.map(node.nodes, &render_node(&1, node)) - ["<ol class=\"list-decimal pl-6 my-4\">", content, "</ol>"] - end - - # List items - defp render_node(%MDEx.ListItem{} = node, _parent) do - content = Enum.map(node.nodes, &render_node(&1, node)) - ["<li class=\"mb-1\">", content, "</li>"] - end - - # Task items (checkboxes) - defp render_node(%MDEx.TaskItem{checked: checked} = node, _parent) do - checkbox = - if checked do - "<input type=\"checkbox\" checked disabled class=\"rounded mr-1.5 text-blue-600\">" - else - "<input type=\"checkbox\" disabled class=\"rounded mr-1.5 text-blue-600\">" - end - - content = Enum.map(node.nodes, &render_node(&1, node)) - - [ - "<li class=\"mb-1 flex items-start\">", - checkbox, - "<div class=\"inline\">", - content, - "</div>", - "</li>" - ] - end - - # Blockquote - defp render_node(%MDEx.BlockQuote{} = node, _parent) do - content = Enum.map(node.nodes, &render_node(&1, node)) - - [ - "<blockquote class=\"border-l-4 border-gray-300 pl-4 py-1 my-4 italic\">", - content, - "</blockquote>" - ] - end - - # Horizontal Rule - defp render_node(%MDEx.ThematicBreak{}, _parent) do - "<hr class=\"border-t border-gray-300 my-6\">" - end - - # Table - defp render_node(%MDEx.Table{} = node, _parent) do - content = Enum.map(node.nodes, &render_node(&1, node)) - - [ - "<div class=\"overflow-x-auto my-4\"><table class=\"min-w-full divide-y divide-gray-200\">", - content, - "</table></div>" - ] - end - - # Table row - defp render_node(%MDEx.TableRow{header: true} = node, _parent) do - content = Enum.map(node.nodes, &render_node(&1, node)) - ["<tr class=\"bg-gray-50 border-b border-gray-200\">", content, "</tr>"] - end - - defp render_node(%MDEx.TableRow{header: false} = node, _parent) do - content = Enum.map(node.nodes, &render_node(&1, node)) - ["<tr class=\"border-b border-gray-200\">", content, "</tr>"] - end - - # Table cell in header row - defp render_node(%MDEx.TableCell{} = node, %MDEx.TableRow{header: true}) do - content = Enum.map(node.nodes, &render_node(&1, node)) - - [ - "<th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">", - content, - "</th>" - ] - end - - # Table cell in normal row - defp render_node(%MDEx.TableCell{} = node, _parent) do - content = Enum.map(node.nodes, &render_node(&1, node)) - ["<td class=\"px-6 py-4 whitespace-nowrap text-sm\">", content, "</td>"] - end - - # HTML blocks (pass through) - defp render_node(%MDEx.HtmlBlock{literal: literal}, _parent) do - literal - end - - # HTML inline (pass through) - defp render_node(%MDEx.HtmlInline{literal: literal}, _parent) do - literal - end - - # Soft break (line break that doesn't create new paragraph) - defp render_node(%MDEx.SoftBreak{}, _parent) do - "\n" - end - - # Hard break (explicit line break with <br>) - defp render_node(%MDEx.LineBreak{}, _parent) do - "<br>" - end - - # Footnote reference - defp render_node(%MDEx.FootnoteReference{name: name}, _parent) do - [ - "<sup class=\"footnote-ref\">", - "<a href=\"#fn-", - name, - "\" id=\"fnref-", - name, - "\" class=\"text-blue-600 hover:text-blue-800\">", - name, - "</a>", - "</sup>" - ] - end - - # Footnote definition - defp render_node(%MDEx.FootnoteDefinition{name: name} = node, _parent) do - content = Enum.map(node.nodes, &render_node(&1, node)) - - [ - "<div class=\"footnote flex flex-row\" id=\"fn-", - name, - "\">", - "<p class=\"my-1 flex\">", - "<span class=\"mr-2 flex-shrink-0\"><sup>", - name, - ".</sup></span>", - "<span class=\"inline\">", - content, - " <a href=\"#fnref-", - name, - "\" class=\"text-blue-600 hover:text-blue-800 text-sm\">↩</a>", - "</span>", - "</p>", - "</div>" - ] - end - - # Extract plain text from a node tree (for generating slugs) - defp extract_text_content(node) do - cond do - is_map(node) && Map.has_key?(node, :literal) && is_binary(node.literal) -> - node.literal - - is_map(node) && Map.has_key?(node, :nodes) -> - node.nodes - |> Enum.map(&extract_text_content/1) - |> Enum.join("") - - true -> - "" - end - end end