feat(join): render join_description with auto-linked URLs and Markdown links
This commit is contained in:
parent
b6c2cf58b1
commit
cb5cb68483
2 changed files with 155 additions and 0 deletions
70
lib/mv_web/helpers/join_description_renderer.ex
Normal file
70
lib/mv_web/helpers/join_description_renderer.ex
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue