NimblePublisher
Mix.install([
{:mdex, "~> 0.11"},
{:phoenix_live_view, "~> 1.0"},
{:phoenix_playground, "~> 0.1"},
{:nimble_publisher, "~> 1.1"},
{:phoenix_html, "~> 4.2"},
{:req_embed, "~> 0.3"}
])
Example
By @PJUllrich
defmodule MDEx.Posts.Post do
@enforce_keys [:id, :title, :date, :body]
defstruct [:id, :title, :date, :body]
def build(filepath, attrs, body) do
[year, month, day, id] =
filepath |> Path.rootname() |> Path.split() |> List.last() |> String.split("-", parts: 4)
id =
id
|> String.trim_trailing(".md")
|> String.downcase()
date = Date.from_iso8601!("#{year}-#{month}-#{day}")
struct!(__MODULE__, Map.merge(attrs, %{id: id, date: date, body: body}))
end
end
{:module, MDEx.Posts.Post, <<70, 79, 82, 49, 0, 0, 19, ...>>, ...}
defmodule MDEx.Posts.Parser do
def parse(_path, contents) do
[header, markdown_body] = String.split(contents, "---\n", trim: true, parts: 2)
{%{} = attrs, _} = Code.eval_string(header, [])
html_body = markdown_to_html!(markdown_body)
env = __ENV__
ast = EEx.compile_string(
html_body,
engine: Phoenix.LiveView.TagEngine,
file: env.file,
line: env.line + 1,
caller: env,
indentation: 0,
source: html_body,
tag_handler: Phoenix.LiveView.HTMLEngine
)
assigns = %{}
{rendered, _} = Code.eval_quoted(ast, [assigns: assigns], env)
html_body =
rendered
|> Phoenix.HTML.Safe.to_iodata()
|> IO.iodata_to_binary()
{attrs, html_body}
end
defp markdown_to_html!(markdown_body) do
MDEx.to_html!(markdown_body,
[
extension: [
strikethrough: true,
table: true,
autolink: false,
tasklist: true,
superscript: true,
footnotes: true,
description_lists: true,
# need both multiline block quotes and alerts to enable github/gitlab multiline alerts
multiline_block_quotes: true,
alerts: true,
math_dollars: true,
math_code: true,
shortcodes: true,
underline: true,
spoiler: true,
phoenix_heex: true
],
parse: [
relaxed_tasklist_matching: true,
relaxed_autolinks: true
],
render: [
unsafe: true,
escape: false,
# github_pre_lang and full_info_string are required to enable code block decorators
github_pre_lang: true,
full_info_string: true
]
]
)
end
end
{:module, MDEx.Posts.Parser, <<70, 79, 82, 49, 0, 0, 40, ...>>, ...}
defmodule MDEx.Posts.HTMLConverter do
# ⚠️ Important ⚠️
# You need to provide a custom converter, because otherwise NimblePublisher
# will apply their default markdown -> HTML conversion which will
# interfere with MDEx's conversion.
def convert(_extname, body, _attrs, _opts), do: body
end
{:module, MDEx.Posts.HTMLConverter, <<70, 79, 82, 49, 0, 0, 7, ...>>, ...}
defmodule MDEx.Posts do
use NimblePublisher,
# Update this filepath to your application
# and move the `posts` folder inside your `priv` folder.
#
# from: Application.app_dir(:my_app, "priv/posts/*.md"),
from: Path.join([Path.absname(__DIR__), "posts", "*.md"]),
build: MDEx.Posts.Post,
parser: MDEx.Posts.Parser,
html_converter: MDEx.Posts.HTMLConverter,
as: :posts,
highlighters: []
@posts Enum.sort_by(@posts, & &1.date, {:desc, Date})
def all_posts, do: @posts
defmodule NotFoundError do
defexception [:message, plug_status: 404]
end
def get_post_by_id!(id) do
Enum.find(all_posts(), &(&1.id == id)) ||
raise NotFoundError, "post with id=#{id} not found"
end
end
{:module, MDEx.Posts, <<70, 79, 82, 49, 0, 0, 27, ...>>, ...}
defmodule MDEx.DemoLive do
use Phoenix.LiveView
import Phoenix.HTML
def mount(%{"id" => post_id}, _session, socket) do
post = MDEx.Posts.get_post_by_id!(post_id)
{:ok, assign(socket, :post, post)}
end
# Show the first blog post by default.
# In your app, you'd show an overview of the blog posts instead.
def mount(_params, session, socket) do
mount(%{"id" => "example-post"}, session, socket)
end
def render(assigns) do
~H"""
{@post.title}
{@post.date}
{raw(@post.body)}
Running on <.link href="https://github.com/dashbitco/nimble_publisher">https://github.com/dashbitco/nimble_publisher
"""
end
end
{:module, MDEx.DemoLive, <<70, 79, 82, 49, 0, 0, 36, ...>>, ...}
PhoenixPlayground.start(live: MDEx.DemoLive, port: 4000)
22:33:55.736 [info] Running PhoenixPlayground.Endpoint with Bandit 1.10.1 at 127.0.0.1:5051 (http)
22:33:55.737 [info] Access PhoenixPlayground.Endpoint at http://localhost:5051
{:ok, #PID<0.1319.0>}
22:33:55.837 [info] GET /
22:33:55.847 [debug] Processing with PhoenixPlayground.Router.DelegateLive.__live__/0
Parameters: %{}
Pipelines: [:browser]
22:33:57.106 [info] Sent 200 in 1269ms
22:33:57.137 [info] CONNECTED TO Phoenix.LiveView.Socket in 16µs
Transport: :websocket
Serializer: Phoenix.Socket.V2.JSONSerializer
Parameters: %{"vsn" => "2.0.0"}
22:33:57.162 [debug] MOUNT PhoenixPlayground.Router.DelegateLive
Parameters: %{}
Session: %{}
22:33:57.162 [debug] Replied in 139µs
22:33:57.162 [debug] HANDLE PARAMS in PhoenixPlayground.Router.DelegateLive
Parameters: %{}
22:33:57.162 [debug] Replied in 60µs