Powered by AppSignal & Oban Pro

NimblePublisher

examples/nimble_publisher.livemd

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, &amp; &amp;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(), &amp;(&amp;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