examples/codefence_renderers.livemd

<!-- livebook:{"persist_outputs":true} -->

# Codefence Renderers

```elixir
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:

````elixir
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(<div class="alert">#{String.trim(code)}</div>)
      end
    }
  )

Kino.HTML.new(html)
````

## Pikchr Diagrams

[Pikchr](https://pikchr.org) is a PIC-like markup language for diagrams. The [pikchr](https://hex.pm/packages/pikchr) package renders it to SVG via a precompiled NIF — no system dependencies needed.

````elixir
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:

````elixir
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 = "<tr>" <> Enum.map_join(hd(rows), "", &"<th>#{&1}</th>") <> "</tr>"
        body = Enum.map_join(tl(rows), "", fn row ->
          "<tr>" <> Enum.map_join(row, "", &"<td>#{&1}</td>") <> "</tr>"
        end)

        "<table>#{header}#{body}</table>"
      end
    }
  )

Kino.HTML.new(html)
````

## SVG Charts

[svg_charts](https://hex.pm/packages/svg_charts) renders charts to SVG via a precompiled NIF wrapping [charts-rs](https://github.com/vicanso/charts-rs). The chart type and data are specified as JSON directly in the code fence:

````elixir
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:

````elixir
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(<div style="background:#1e1e1e;padding:1rem;border-radius:8px">#{svg}</div>)
        else
          svg
        end
      end
    }
  )

Kino.HTML.new(html)
````