Always open chat links in new tabs
This commit is contained in:
@@ -25,7 +25,7 @@ defmodule MusicLibraryWeb.Components.Chat do
|
|||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def update(%{chunk: chunk}, socket) do
|
def update(%{chunk: chunk}, socket) do
|
||||||
doc = socket.assigns.streaming_doc || Markdown.new_streaming_doc()
|
doc = socket.assigns.streaming_doc || Markdown.new_streaming_doc(link_target: "_blank")
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
@@ -246,7 +246,7 @@ defmodule MusicLibraryWeb.Components.Chat do
|
|||||||
>
|
>
|
||||||
<p :if={message.role == "user"} class="whitespace-pre-wrap">{message.content}</p>
|
<p :if={message.role == "user"} class="whitespace-pre-wrap">{message.content}</p>
|
||||||
<div :if={message.role == "assistant"} class="dark:prose-invert prose prose-sm">
|
<div :if={message.role == "assistant"} class="dark:prose-invert prose prose-sm">
|
||||||
{raw(Markdown.to_html(message.content))}
|
{raw(Markdown.to_html(message.content, link_target: "_blank"))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -255,7 +255,7 @@ defmodule MusicLibraryWeb.Components.Chat do
|
|||||||
class="max-w-[85%] rounded-lg bg-zinc-100 px-4 py-2 text-sm text-zinc-900 dark:bg-zinc-700 dark:text-zinc-100"
|
class="max-w-[85%] rounded-lg bg-zinc-100 px-4 py-2 text-sm text-zinc-900 dark:bg-zinc-700 dark:text-zinc-100"
|
||||||
>
|
>
|
||||||
<div class="dark:prose-invert prose prose-sm">
|
<div class="dark:prose-invert prose prose-sm">
|
||||||
{raw(Markdown.streaming_to_html(@streaming_doc))}
|
{raw(Markdown.streaming_to_html(@streaming_doc, link_target: "_blank"))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -14,46 +14,67 @@ defmodule MusicLibraryWeb.Markdown do
|
|||||||
sanitize: MDEx.Document.default_sanitize_options()
|
sanitize: MDEx.Document.default_sanitize_options()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@link_target_sanitize_options Keyword.put(
|
||||||
|
MDEx.Document.default_sanitize_options(),
|
||||||
|
:add_tag_attributes,
|
||||||
|
%{"a" => ["target"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
@link_target_mdex_options Keyword.put(@mdex_options, :sanitize, @link_target_sanitize_options)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Converts markdown with custom `[[link]]` syntax to HTML.
|
Converts markdown with custom `[[link]]` syntax to HTML.
|
||||||
|
|
||||||
Double square brackets are converted to search links before the markdown is processed.
|
Double square brackets are converted to search links before the markdown is processed.
|
||||||
"""
|
"""
|
||||||
@spec to_html(String.t() | nil) :: String.t()
|
@spec to_html(String.t() | nil, keyword()) :: String.t()
|
||||||
def to_html(markdown_text) when is_binary(markdown_text) do
|
def to_html(markdown_text, opts \\ [])
|
||||||
|
|
||||||
|
def to_html(markdown_text, opts) when is_binary(markdown_text) do
|
||||||
:telemetry.span(
|
:telemetry.span(
|
||||||
[:markdown, :to_html],
|
[:markdown, :to_html],
|
||||||
%{},
|
%{},
|
||||||
fn ->
|
fn ->
|
||||||
|
processed = process_double_bracket_links(markdown_text)
|
||||||
|
|
||||||
result =
|
result =
|
||||||
markdown_text
|
if opts[:link_target] do
|
||||||
|> process_double_bracket_links()
|
processed
|
||||||
|> MDEx.to_html!(@mdex_options)
|
|> MDEx.parse_document!(@link_target_mdex_options)
|
||||||
|
|> open_links_in_new_tab(opts[:link_target])
|
||||||
|
|> MDEx.to_html!()
|
||||||
|
else
|
||||||
|
MDEx.to_html!(processed, @mdex_options)
|
||||||
|
end
|
||||||
|
|
||||||
{result, %{}}
|
{result, %{}}
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_html(nil), do: ""
|
def to_html(nil, _opts), do: ""
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a new streaming MDEx document for incremental markdown rendering.
|
Creates a new streaming MDEx document for incremental markdown rendering.
|
||||||
"""
|
"""
|
||||||
@spec new_streaming_doc() :: MDEx.Document.t()
|
@spec new_streaming_doc(keyword()) :: MDEx.Document.t()
|
||||||
def new_streaming_doc do
|
def new_streaming_doc(opts \\ []) do
|
||||||
MDEx.new([streaming: true] ++ @mdex_options)
|
mdex_options = if opts[:link_target], do: @link_target_mdex_options, else: @mdex_options
|
||||||
|
MDEx.new([streaming: true] ++ mdex_options)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Renders a streaming MDEx document to HTML.
|
Renders a streaming MDEx document to HTML.
|
||||||
"""
|
"""
|
||||||
@spec streaming_to_html(MDEx.Document.t()) :: String.t()
|
@spec streaming_to_html(MDEx.Document.t(), keyword()) :: String.t()
|
||||||
def streaming_to_html(%MDEx.Document{} = doc) do
|
def streaming_to_html(doc, opts \\ [])
|
||||||
|
|
||||||
|
def streaming_to_html(%MDEx.Document{} = doc, opts) do
|
||||||
:telemetry.span(
|
:telemetry.span(
|
||||||
[:markdown, :streaming_to_html],
|
[:markdown, :streaming_to_html],
|
||||||
%{},
|
%{},
|
||||||
fn ->
|
fn ->
|
||||||
|
doc = if opts[:link_target], do: open_links_in_new_tab(doc, opts[:link_target]), else: doc
|
||||||
result = MDEx.to_html!(doc)
|
result = MDEx.to_html!(doc)
|
||||||
{result, %{}}
|
{result, %{}}
|
||||||
end
|
end
|
||||||
@@ -97,4 +118,31 @@ defmodule MusicLibraryWeb.Markdown do
|
|||||||
_ -> content
|
_ -> content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp open_links_in_new_tab(doc, target) do
|
||||||
|
MDEx.Document.update_nodes(doc, MDEx.Link, fn %MDEx.Link{url: url, title: title, nodes: nodes} ->
|
||||||
|
text = extract_text(nodes)
|
||||||
|
|
||||||
|
title_attr =
|
||||||
|
if title != "" and title != nil, do: ~s( title="#{html_escape(title)}"), else: ""
|
||||||
|
|
||||||
|
%MDEx.HtmlInline{
|
||||||
|
literal:
|
||||||
|
~s(<a href="#{html_escape(url)}" target="#{target}"#{title_attr}>#{html_escape(text)}</a>)
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp extract_text(nodes) when is_list(nodes), do: Enum.map_join(nodes, "", &extract_text/1)
|
||||||
|
defp extract_text(%{literal: literal}), do: literal
|
||||||
|
defp extract_text(%{nodes: nodes}), do: extract_text(nodes)
|
||||||
|
defp extract_text(_), do: ""
|
||||||
|
|
||||||
|
defp html_escape(text) do
|
||||||
|
text
|
||||||
|
|> String.replace("&", "&")
|
||||||
|
|> String.replace("<", "<")
|
||||||
|
|> String.replace(">", ">")
|
||||||
|
|> String.replace("\"", """)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -58,5 +58,43 @@ defmodule MusicLibraryWeb.MarkdownTest do
|
|||||||
assert result =~ "http://example.com"
|
assert result =~ "http://example.com"
|
||||||
assert result =~ "link</a>"
|
assert result =~ "link</a>"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "does not add target attribute by default" do
|
||||||
|
result = Markdown.to_html("[link](http://example.com)")
|
||||||
|
|
||||||
|
refute result =~ "target="
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "to_html/2 with link_target" do
|
||||||
|
test "adds target and rel attributes to links" do
|
||||||
|
result = Markdown.to_html("[link](http://example.com)", link_target: "_blank")
|
||||||
|
|
||||||
|
assert result =~ ~s(target="_blank")
|
||||||
|
assert result =~ ~s(rel="noopener noreferrer")
|
||||||
|
assert result =~ ~s(href="http://example.com")
|
||||||
|
assert result =~ ">link</a>"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "adds target to autolinked URLs" do
|
||||||
|
result = Markdown.to_html("Visit http://example.com for more", link_target: "_blank")
|
||||||
|
|
||||||
|
assert result =~ ~s(target="_blank")
|
||||||
|
assert result =~ ~s(href="http://example.com")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "adds target to double bracket links" do
|
||||||
|
result = Markdown.to_html("Check [[Porcupine Tree]]", link_target: "_blank")
|
||||||
|
|
||||||
|
assert result =~ ~s(target="_blank")
|
||||||
|
assert result =~ "Porcupine Tree</a>"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "preserves link title" do
|
||||||
|
result = Markdown.to_html(~s|[link](http://example.com "My Title")|, link_target: "_blank")
|
||||||
|
|
||||||
|
assert result =~ ~s(title="My Title")
|
||||||
|
assert result =~ ~s(target="_blank")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user