70 lines
2.3 KiB
Elixir
70 lines
2.3 KiB
Elixir
defmodule MvWeb.Helpers.JoinDescriptionRenderer do
|
|
@moduledoc """
|
|
Renders a custom field's `join_description` into Phoenix-safe HTML for the
|
|
public join form.
|
|
|
|
The renderer auto-links two patterns into `<a href="...">` tags:
|
|
|
|
- Markdown links of the form `[text](url)` (processed first)
|
|
- bare `http(s)://` URLs in the remaining text
|
|
|
|
All other content is HTML-escaped: only `<a href="...">` tags are ever
|
|
emitted, so arbitrary HTML in the input is rendered as inert text. This is a
|
|
defense-in-depth measure — `join_description` is admin-set content, never
|
|
end-user input — but the renderer must not become a vector for injecting
|
|
arbitrary markup.
|
|
|
|
Markdown links are matched before bare URLs and their matched region is
|
|
consumed, so a Markdown link whose URL also looks like a bare URL is linked
|
|
exactly once (no nested anchors).
|
|
"""
|
|
|
|
@markdown_link ~r/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/
|
|
@bare_url ~r/(https?:\/\/[^\s<]+)/
|
|
@bare_url_anchored ~r/\A(https?:\/\/[^\s<]+)\z/
|
|
|
|
@doc """
|
|
Converts `value` to a Phoenix-safe HTML iolist.
|
|
|
|
Returns `{:safe, ""}` for `nil`. For a string, returns `{:safe, iolist}` with
|
|
links rendered and all other text HTML-escaped.
|
|
"""
|
|
@spec render(String.t() | nil) :: Phoenix.HTML.safe()
|
|
def render(nil), do: {:safe, ""}
|
|
|
|
def render(value) when is_binary(value) do
|
|
{:safe, render_segments(value)}
|
|
end
|
|
|
|
# Split on Markdown links first; for each non-Markdown segment, link bare URLs;
|
|
# everything that is not a link is HTML-escaped.
|
|
defp render_segments(text) do
|
|
Regex.split(@markdown_link, text, include_captures: true)
|
|
|> Enum.map(&render_markdown_or_plain/1)
|
|
end
|
|
|
|
defp render_markdown_or_plain(segment) do
|
|
case Regex.run(@markdown_link, segment) do
|
|
[^segment, label, url] -> anchor(url, label)
|
|
_ -> render_plain(segment)
|
|
end
|
|
end
|
|
|
|
# Auto-link bare URLs in a plain-text segment, escaping all surrounding text.
|
|
defp render_plain(segment) do
|
|
Regex.split(@bare_url, segment, include_captures: true)
|
|
|> Enum.map(fn part ->
|
|
if Regex.match?(@bare_url_anchored, part) do
|
|
anchor(part, part)
|
|
else
|
|
escape(part)
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp anchor(url, label) do
|
|
["<a href=\"", escape(url), "\">", escape(label), "</a>"]
|
|
end
|
|
|
|
defp escape(text), do: Phoenix.HTML.html_escape(text) |> Phoenix.HTML.safe_to_string()
|
|
end
|