`
* `id` — unique ID prefix (auto-generated; pass explicitly for stable IDs across re-renders)
* `theme` — syntax highlighting theme name (default: `"onedark"`)
* `mdex_opts` — options passed to `MDEx.to_html!/2` (merged with defaults)
## Examples
"""
attr :content, :string, default: ""
attr :streaming, :boolean, default: false
attr :class, :any, default: nil
attr :block_class, :any, default: nil
attr :id, :string
attr :theme, :string, default: "onedark"
attr :mdex_opts, :list, default: []
def markdown(assigns) do
assigns = assign_new(assigns, :id, fn -> "psd-#{System.unique_integer([:positive])}" end)
completed =
if assigns.streaming do
Remend.complete(assigns.content || "")
else
assigns.content || ""
end
blocks =
completed
|> Blocks.parse()
|> Enum.with_index()
mdex_opts = build_mdex_opts(assigns.mdex_opts, assigns.theme)
last_idx = length(blocks) - 1
rendered_blocks =
Enum.map(blocks, fn {block, idx} ->
{idx, render_block(block, mdex_opts), idx == last_idx}
end)
assigns =
assigns
|> assign(:rendered_blocks, rendered_blocks)
|> assign(:last_idx, last_idx)
~H"""
"""
end
defp build_mdex_opts(user_opts, theme) do
theme_opts = [
syntax_highlight: [
formatter: {:html_inline, [theme: theme]}
]
]
@default_mdex_opts
|> deep_merge(theme_opts)
|> deep_merge(user_opts)
end
defp deep_merge(base, override) do
Keyword.merge(base, override, fn _key, v1, v2 ->
if Keyword.keyword?(v1) and Keyword.keyword?(v2) do
deep_merge(v1, v2)
else
v2
end
end)
end
defp render_block(block, mdex_opts) do
MDEx.to_html!(block, mdex_opts)
rescue
_ -> "
#{Phoenix.HTML.html_escape(block)}
"
end
end