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 `` 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 `` 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 ["", escape(label), ""] end defp escape(text), do: Phoenix.HTML.html_escape(text) |> Phoenix.HTML.safe_to_string() end