NimblePublisher
Mix.install([
{:mdex, "~> 0.8"},
{:phoenix_live_view, "~> 1.0"},
{:phoenix_playground, "~> 0.1"},
{:nimble_publisher, "~> 1.1"},
{:phoenix_html, "~> 4.2"}
])
Example
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, 18, ...>>, {:build, 3}}
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)
{attrs, html_body}
end
defp markdown_to_html!(markdown_body) do
MDEx.to_html!(markdown_body,
syntax_highlight: [formatter: {:html_inline, theme: "github_dark"}],
extension: [
strikethrough: true,
underline: true,
tagfilter: true,
table: true,
autolink: true,
tasklist: true,
footnotes: true,
shortcodes: true
],
parse: [
smart: true,
relaxed_tasklist_matching: true,
relaxed_autolinks: true
],
render: [
github_pre_lang: true,
escape: true,
hardbreaks: true
]
)
end
end
{:module, MDEx.Posts.Parser, <<70, 79, 82, 49, 0, 0, 11, ...>>, {:markdown_to_html!, 1}}
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, ...>>, {:convert, 4}}
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, 21, ...>>, {:get_post_by_id!, 1}}
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)}
"""
end
end
{:module, MDEx.DemoLive, <<70, 79, 82, 49, 0, 0, 24, ...>>, {:render, 1}}
PhoenixPlayground.start(live: MDEx.DemoLive)
19:29:52.699 [info] Running PhoenixPlayground.Endpoint with Bandit 1.7.0 at 127.0.0.1:4000 (http)
19:29:52.704 [info] Access PhoenixPlayground.Endpoint at http://localhost:4000
19:29:52.731 [info] CONNECTED TO Phoenix.LiveView.Socket in 16µs
Transport: :websocket
Serializer: Phoenix.Socket.V2.JSONSerializer
Parameters: %{"vsn" => "2.0.0"}
{:ok, #PID<0.343.0>}
19:29:52.781 [debug] MOUNT PhoenixPlayground.Router.DelegateLive
Parameters: %{}
Session: %{}
19:29:52.781 [debug] Replied in 115µs
19:29:52.783 [debug] HANDLE PARAMS in PhoenixPlayground.Router.DelegateLive
Parameters: %{}
19:29:52.783 [debug] Replied in 61µs
19:29:52.801 [info] GET /
19:29:52.805 [debug] Processing with PhoenixPlayground.Router.DelegateLive.index/2
Parameters: %{}
Pipelines: [:browser]
19:29:52.816 [info] Sent 200 in 15ms
19:29:52.843 [info] CONNECTED TO Phoenix.LiveView.Socket in 18µs
Transport: :websocket
Serializer: Phoenix.Socket.V2.JSONSerializer
Parameters: %{"vsn" => "2.0.0"}
19:29:52.844 [debug] MOUNT PhoenixPlayground.Router.DelegateLive
Parameters: %{}
Session: %{}
19:29:52.844 [debug] Replied in 101µs
19:29:52.844 [debug] HANDLE PARAMS in PhoenixPlayground.Router.DelegateLive
Parameters: %{}
19:29:52.844 [debug] Replied in 21µs