From f62776a5f4b34fc53c5038614760426c5ffc3e60 Mon Sep 17 00:00:00 2001 From: Austin Ziegler Date: Wed, 24 Dec 2025 23:33:59 -0500 Subject: [PATCH 1/5] feat!: update to MDEx 0.10 MDEx 0.10 has a few breaking changes which should have minimal impact on Tableau, but the larger change is that there is explicit support for plugins in MDEx, using the similar patterns as Req does. To simplify the use of plugins in Tableau, I have added a `plugins` key to `config.markdown.mdex` which is extracted and used to attach MDEx plugin modules prior to calling `MDEx.to_html!()`. This _could_ be implemented as a separate key `config.mdex_plugins`, but since it's part of the markdown converter, this felt more natural to me. If MDEx or one of its plugins adds a `plugins` option, this will break that -- but I think it's low probability. --- lib/tableau.ex | 10 +++--- lib/tableau/converters/mdex_converter.ex | 41 ++++++++++++++++++++++-- mix.exs | 2 +- mix.lock | 8 ++--- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/lib/tableau.ex b/lib/tableau.ex index e696dda..8912f41 100644 --- a/lib/tableau.ex +++ b/lib/tableau.ex @@ -10,6 +10,7 @@ defmodule Tableau do * `:converters` - mapping of file extensions to converter module. Defaults to `[md: Tableau.MDExConverter]` * `:markdown` - keyword * `:mdex` - keyword - Options to pass to `MDEx.to_html/2` + * `:plugins` - module list - MDEx plugin modules to attach to the processing pipeline * `:slug` - keyword - Options to pass to `Slug.slugify/2` ### Example @@ -41,7 +42,8 @@ defmodule Tableau do footnotes: true ], render: [unsafe: true], - syntax_highlight: [formatter: {:html_inline, theme: "neovim_dark"}] + syntax_highlight: [formatter: {:html_inline, theme: "neovim_dark"}], + plugins: [MDExGFM] ] ] ``` @@ -61,9 +63,5 @@ defmodule Tableau do Will use the globally configured options, but you can also pass it overrides. """ - def markdown(content, overrides \\ []) do - {:ok, config} = Tableau.Config.get() - - MDEx.to_html!(content, Keyword.merge(config.markdown[:mdex], overrides)) - end + defdelegate markdown(content, overrides \\ []), to: Tableau.MDExConverter end diff --git a/lib/tableau/converters/mdex_converter.ex b/lib/tableau/converters/mdex_converter.ex index 597e773..0f0eb87 100644 --- a/lib/tableau/converters/mdex_converter.ex +++ b/lib/tableau/converters/mdex_converter.ex @@ -1,8 +1,45 @@ defmodule Tableau.MDExConverter do @moduledoc """ - Converter to parse markdown content with `MDEx` + Converter to parse markdown content with `MDEx` with support for MDEx plugins. """ + + @doc """ + Convert markdown content to HTML using `MDEx.to_html!/2`. + + Will use the globally configured options, but you can also pass it overrides. + """ + def markdown(content, overrides \\ []) do + {:ok, config} = Tableau.Config.get() + + {plugins, mdex_config} = resolve_plugins(config, overrides) + + render!(content, mdex_config, plugins) + end + def convert(_filepath, _front_matter, body, %{site: %{config: config}}) do - MDEx.to_html!(body, config.markdown[:mdex]) + {plugins, mdex_config} = resolve_plugins(config) + + render!(body, mdex_config, plugins) + end + + defp resolve_plugins(config, overrides \\ []) do + config.markdown[:mdex] + |> Keyword.merge(overrides, fn + :plugins, left, right -> List.wrap(right) ++ List.wrap(left) + _, _, v -> v + end) + |> Keyword.pop(:plugins, []) + end + + defp render!(content, mdex_config, plugins) do + mdex_config + |> Keyword.put(:markdown, content) + |> MDEx.new() + |> attach_plugins(plugins) + |> MDEx.to_html!() + end + + defp attach_plugins(mdex, plugins) do + Enum.reduce(plugins, mdex, fn mod, mdex -> mod.attach(mdex) end) end end diff --git a/mix.exs b/mix.exs index 3dd4126..8dc5ce9 100644 --- a/mix.exs +++ b/mix.exs @@ -37,7 +37,7 @@ defmodule Tableau.MixProject do {:date_time_parser, "~> 1.2"}, {:html_entities, "~> 0.5.2"}, {:libgraph, "~> 0.16.0"}, - {:mdex, "~> 0.9.0"}, + {:mdex, "~> 0.10.0"}, {:schematic, "~> 0.5.1"}, {:slugify, "~> 1.3"}, {:tz, "~> 0.28.1"}, diff --git a/mix.lock b/mix.lock index 13b0d81..8477d74 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ - "autumn": {:hex, :autumn, "0.5.5", "05cda4e2b79957c8540eb0184f1ac00fba187a6dabd8461e78c40f9fc8417f2d", [:mix], [{:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:rustler, "~> 0.29", [hex: :rustler, repo: "hexpm", optional: false]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "49e40b50e16fc49dcbf0bd4071b32e5f64755403c8073490aff2a536d442df36"}, + "autumn": {:hex, :autumn, "0.5.7", "f6bfdc30d3f8d5e82ba5648489db7a7b6b7479d7be07a8288d4db2437434e26d", [:mix], [{:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:rustler, "~> 0.29", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "d272bfddeeea863420a8eb994d42af219ca5391191dd765bf045fbacf56a28d1"}, "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"}, - "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"}, + "castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"}, "date_time_parser": {:hex, :date_time_parser, "1.2.0", "3d5a816b91967f51e0f94dcb16a34b2cb780f22cd48931779e81d72f7d3eadb1", [:mix], [{:kday, "~> 1.0", [hex: :kday, repo: "hexpm", optional: false]}], "hexpm", "0cf09ada9f42c0b3bfba02dc0ea2e4b4d2f543d9d2bf99b831a29e6b4a4160e5"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"}, @@ -15,14 +15,14 @@ "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, - "mdex": {:hex, :mdex, "0.9.2", "d70e0a9116105733d7999cbbfcb52823b6e6a4b02174205f0d0815cb1ed68dd4", [:mix], [{:autumn, ">= 0.5.4", [hex: :autumn, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: false]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "d5a3371ec939141d971ed500bdbf3a23a22a91df80753356b256ec13dfadfd13"}, + "mdex": {:hex, :mdex, "0.10.0", "eae4d3bd4c0b77d6d959146a2d6faaec045686548ad1468630130095dbd93def", [:mix], [{:autumn, ">= 0.5.4", [hex: :autumn, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: false]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "6ad76e32056c44027fe985da7da506e033b07037896d1f130f7d5c332b0d0ac0"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "rustler": {:hex, :rustler, "0.37.1", "721434020c7f6f8e1cdc57f44f75c490435b01de96384f8ccb96043f12e8a7e0", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "24547e9b8640cf00e6a2071acb710f3e12ce0346692e45098d84d45cdb54fd79"}, - "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.3", "4e741024b0b097fe783add06e53ae9a6f23ddc78df1010f215df0c02915ef5a8", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "c23f5f33cb6608542de4d04faf0f0291458c352a4648e4d28d17ee1098cddcc4"}, + "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.4", "700a878312acfac79fb6c572bb8b57f5aae05fe1cf70d34b5974850bbf2c05bf", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "3b33d99b540b15f142ba47944f7a163a25069f6d608783c321029bc1ffb09514"}, "schematic": {:hex, :schematic, "0.5.1", "be4b2c03115d5a593459c11a7249a6fbb45855947d9653e9250455dcd7df1d42", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02f913c97e6e04ccdaa02004679a7a16bb16fe0449583ad647e296d8e8961546"}, "slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"}, "styler": {:hex, :styler, "1.1.1", "ccb55763316915b5de532bf14c587c211ddc86bc749ac676e74dfacd3894cc0d", [:mix], [], "hexpm", "80ce12fb862e13d998589eea7c1932f4e6ce9d6ded2182cb322f8f9b2b8d3632"}, From 1666e2a94ef885c4bb8116e027e748fdd797c737 Mon Sep 17 00:00:00 2001 From: Austin Ziegler Date: Thu, 8 Jan 2026 09:52:22 -0500 Subject: [PATCH 2/5] Clarify that `:plugins` is non-standard and Tableau only Co-authored-by: Mitchell Hanberg --- lib/tableau.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tableau.ex b/lib/tableau.ex index 8912f41..7fa393d 100644 --- a/lib/tableau.ex +++ b/lib/tableau.ex @@ -9,7 +9,7 @@ defmodule Tableau do * `:url` - string (required) - The URL of your website. * `:converters` - mapping of file extensions to converter module. Defaults to `[md: Tableau.MDExConverter]` * `:markdown` - keyword - * `:mdex` - keyword - Options to pass to `MDEx.to_html/2` + * `:mdex` - keyword - Options to pass to `MDEx.to_html/2`. See `MDEx` documentation for list of options, as options listed here are non-standard and specific to Tableau. * `:plugins` - module list - MDEx plugin modules to attach to the processing pipeline * `:slug` - keyword - Options to pass to `Slug.slugify/2` From dd0b230264540f95de3de2b22961f14fa1f67e99 Mon Sep 17 00:00:00 2001 From: Austin Ziegler Date: Thu, 8 Jan 2026 10:10:57 -0500 Subject: [PATCH 3/5] Refactor plugin resolution Refactor `MDExConverter.markdown/2` and `MDExConverter.convert/4` functions to use Keyword.pop for plugins. `MDExConverter.markdown/2` no longer tries to *merge* `:plugins`. --- lib/tableau/converters/mdex_converter.ex | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/lib/tableau/converters/mdex_converter.ex b/lib/tableau/converters/mdex_converter.ex index 0f0eb87..df72fc5 100644 --- a/lib/tableau/converters/mdex_converter.ex +++ b/lib/tableau/converters/mdex_converter.ex @@ -11,26 +11,19 @@ defmodule Tableau.MDExConverter do def markdown(content, overrides \\ []) do {:ok, config} = Tableau.Config.get() - {plugins, mdex_config} = resolve_plugins(config, overrides) + {plugins, mdex_config} = + config.markdown[:mdex] + |> Keyword.merge(overrides) + |> Keyword.pop(:plugins, []) render!(content, mdex_config, plugins) end def convert(_filepath, _front_matter, body, %{site: %{config: config}}) do - {plugins, mdex_config} = resolve_plugins(config) - + {plugins, mdex_config} = Keyword.pop(:plugins, config.markdown[:mdex]) render!(body, mdex_config, plugins) end - defp resolve_plugins(config, overrides \\ []) do - config.markdown[:mdex] - |> Keyword.merge(overrides, fn - :plugins, left, right -> List.wrap(right) ++ List.wrap(left) - _, _, v -> v - end) - |> Keyword.pop(:plugins, []) - end - defp render!(content, mdex_config, plugins) do mdex_config |> Keyword.put(:markdown, content) From 27071c23d120c39ae0b9ba752f2a3d45ac5431e1 Mon Sep 17 00:00:00 2001 From: Austin Ziegler Date: Thu, 8 Jan 2026 10:12:11 -0500 Subject: [PATCH 4/5] Improve MDEx plugin attach calls Ensure that the code is loaded before trying to call `mod.attach/1`. --- lib/tableau/converters/mdex_converter.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/tableau/converters/mdex_converter.ex b/lib/tableau/converters/mdex_converter.ex index df72fc5..9d6e341 100644 --- a/lib/tableau/converters/mdex_converter.ex +++ b/lib/tableau/converters/mdex_converter.ex @@ -33,6 +33,9 @@ defmodule Tableau.MDExConverter do end defp attach_plugins(mdex, plugins) do - Enum.reduce(plugins, mdex, fn mod, mdex -> mod.attach(mdex) end) + Enum.reduce(plugins, mdex, fn mod, mdex -> + Code.ensure_loaded!(mod) + mod.attach(mdex) + end) end end From b7064122f7b1e55b9e255372366d25ed034fbafa Mon Sep 17 00:00:00 2001 From: Austin Ziegler Date: Thu, 8 Jan 2026 10:14:07 -0500 Subject: [PATCH 5/5] Fix argument order in Keyword.pop for mdex config --- lib/tableau/converters/mdex_converter.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tableau/converters/mdex_converter.ex b/lib/tableau/converters/mdex_converter.ex index 9d6e341..28cca7b 100644 --- a/lib/tableau/converters/mdex_converter.ex +++ b/lib/tableau/converters/mdex_converter.ex @@ -20,7 +20,7 @@ defmodule Tableau.MDExConverter do end def convert(_filepath, _front_matter, body, %{site: %{config: config}}) do - {plugins, mdex_config} = Keyword.pop(:plugins, config.markdown[:mdex]) + {plugins, mdex_config} = Keyword.pop(config.markdown[:mdex], :plugins, []) render!(body, mdex_config, plugins) end