remove custom markdown document -> html, tailwind prose

This commit is contained in:
sloane 2025-04-16 08:08:22 -04:00
parent 123f45c18d
commit d403675d9a
Signed by: sloanelybutsurely
SSH key fingerprint: SHA256:8SBnwhl+RY3oEyQxy1a9wByPzxWM0x+/Ejc+sIlY5qQ
3 changed files with 14 additions and 317 deletions
lib/web
components
controllers/page_html
markdown.ex

View file

@ -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>
"""

View file

@ -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>

View file

@ -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