defmodule PreDotHn.Markdown do @default_opts [pure_links: true, wikilinks: true, inner_html: false] def render(source, opts \\ []) do opts = Keyword.merge(@default_opts, opts, fn _key, _default_value, value -> value end) inner_html = Keyword.get(opts, :inner_html, false) EarmarkParser.as_ast(source || "", opts) |> case do {:ok, ast, _} -> ast |> Earmark.Transform.map_ast(&transformer/1) |> walk() |> Enum.map(&maybe_remove_para(&1, inner_html)) other -> other end |> Earmark.Transform.transform() end defp transformer({"a", attrs, ignored, meta}) do href = Enum.find_value(attrs, "", fn {key, value} -> key == "href" && value end) href_uri = URI.parse(href) is_internal = href_uri.host in [host(), nil] attrs = if is_internal do attrs else [{"target", "_blank"} | attrs] end {"a", attrs, ignored, meta} end defp transformer(other), do: other def walk(list) when is_list(list), do: Enum.map(list, &walk/1) def walk(binary) when is_binary(binary), do: binary def walk({"pre", _attrs, [{"code", attrs, [children], _}], _meta}) do lang = Earmark.AstTools.find_att_in_node(attrs, "class") walk(highlight(children, lang)) end def walk({tag, attrs, children, meta}), do: {tag, attrs, walk(children), meta} defp maybe_remove_para(node, false), do: node defp maybe_remove_para({"p", _attrs, children, _meta}, true), do: Enum.map(children, &add_trailing_newline/1) defp maybe_remove_para(other, true), do: other defp highlight([text], lang), do: highlight(text, lang) defp highlight(text, lang) do {:ok, highlight} = TreeSitter.highlight_html(text, lang || "plain") [_preamble, rest] = String.split(highlight, "