Powered by AppSignal & Oban Pro

Codefence Renderers

examples/codefence_renderers.livemd

Codefence Renderers

Mix.install([
  {:mdex, ">= 0.11.6"},
  {:pikchr, "~> 0.5"},
  {:svg_charts, "~> 0.5.0"},
  {:kino, "~> 0.16"}
])

Intro

Codefence renderers let you plug in custom renderers for fenced code blocks based on the language identifier. Any function that takes (lang, meta, code) and returns an HTML string can be used.

This is useful for rendering diagrams, math, or other specialized content directly in Markdown.

Basic Usage

The simplest example wraps code in a custom HTML element:

markdown = """
# Custom Block

```alert
This is important!
```
"""

html =
  MDEx.to_html!(markdown,
    render: [unsafe: true],
    syntax_highlight: nil,
    codefence_renderers: %{
      "alert" => fn _lang, _meta, code ->
        ~s(#{String.trim(code)})
      end
    }
  )

Kino.HTML.new(html)

Pikchr Diagrams

Pikchr is a PIC-like markup language for diagrams. The pikchr package renders it to SVG via a precompiled NIF — no system dependencies needed.

markdown = """
# System Architecture

```pikchr
right
Client: box "Client" fit
arrow 300% "request" above
Server: box "Server" fit
arrow 300% "query" above
DB: box "DB" fit

arrow from DB.s down 50% then left until even with Server then to Server.s "rows" below
arrow from Server.s down 100% then left until even with Client then to Client.s "response" below
```

Regular Elixir code still gets syntax highlighted:

```elixir
MDEx.to_html!("# Hello")
```
"""

html =
  MDEx.to_html!(markdown,
    render: [unsafe: true],
    codefence_renderers: %{
      "pikchr" => fn _lang, _meta, code -> Pikchr.render!(code) end
    }
  )

Kino.HTML.new(html)

Multiple Renderers

You can register multiple renderers at once. Unregistered languages fall through to the default syntax highlighter:

markdown = """
```pikchr
box "Hello" fit
arrow
box "World" fit
```

```csv
Name,Age
Alice,30
Bob,25
```

```elixir
Enum.map(1..5, & &1 * 2)
```
"""

html =
  MDEx.to_html!(markdown,
    render: [unsafe: true],
    codefence_renderers: %{
      "pikchr" => fn _lang, _meta, code -> Pikchr.render!(code) end,
      "csv" => fn _lang, _meta, code ->
        rows =
          code
          |> String.trim()
          |> String.split("\n")
          |> Enum.map(&String.split(&1, ","))

        header = "" <> Enum.map_join(hd(rows), "", &amp;"#{&amp;1}") <> ""
        body = Enum.map_join(tl(rows), "", fn row ->
          "" <> Enum.map_join(row, "", &amp;"#{&amp;1}") <> ""
        end)

        "#{header}#{body}
"
end } ) Kino.HTML.new(html)

SVG Charts

svg_charts renders charts to SVG via a precompiled NIF wrapping charts-rs. The chart type and data are specified as JSON directly in the code fence:

markdown = """
# Sales Report

```chart
{"type": "bar", "width": 630, "height": 410, "title_text": "Weekly Revenue", "title_height": 30, "legend_margin": {"top": 35}, "series_list": [{"name": "Online", "data": [120.0, 132.0, 101.0, 134.0, 90.0, 230.0, 210.0]}, {"name": "In-Store", "data": [220.0, 182.0, 191.0, 234.0, 290.0, 330.0, 310.0]}], "x_axis_data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]}
```

```chart
{"type": "pie", "width": 500, "height": 400, "title_text": "Traffic Sources", "title_height": 30, "legend_margin": {"top": 35}, "series_list": [{"name": "Search", "data": [1048.0]}, {"name": "Direct", "data": [735.0]}, {"name": "Email", "data": [580.0]}, {"name": "Social", "data": [484.0]}]}
```
"""

html =
  MDEx.to_html!(markdown,
    render: [unsafe: true],
    syntax_highlight: nil,
    codefence_renderers: %{
      "chart" => fn _lang, _meta, code -> SvgCharts.render!(code) end
    }
  )

Kino.HTML.new(html)

Using the Meta String

The info string after the language name is passed as the meta argument. You can use it for configuration:

markdown = """
```pikchr dark
box "Dark" fit
arrow
box "Mode" fit
```
"""

html =
  MDEx.to_html!(markdown,
    render: [unsafe: true],
    codefence_renderers: %{
      "pikchr" => fn _lang, meta, code ->
        opts = if String.contains?(meta, "dark"), do: [dark_mode: true], else: []
        svg = Pikchr.render!(code, opts)

        if String.contains?(meta, "dark") do
          ~s(#{svg})
        else
          svg
        end
      end
    }
  )

Kino.HTML.new(html)