remove custom markdown document -> html, tailwind prose
This commit is contained in:
parent
123f45c18d
commit
d403675d9a
3 changed files with 14 additions and 317 deletions
lib/web
|
@ -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>
|
||||
"""
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue