mirror of
https://github.com/iv-org/invidious.git
synced 2026-10-25 14:29:16 -05:00
Fix clickable hashtags in watch-page titles
This commit is contained in:
parent
049d591d29
commit
c250720fea
72
spec/invidious/frontend/video_title_spec.cr
Normal file
72
spec/invidious/frontend/video_title_spec.cr
Normal file
@ -0,0 +1,72 @@
|
||||
require "../../../src/invidious/frontend/video_title"
|
||||
require "spectator"
|
||||
|
||||
Spectator.configure do |config|
|
||||
config.fail_blank
|
||||
config.randomize
|
||||
end
|
||||
|
||||
Spectator.describe Invidious::Frontend::VideoTitle do
|
||||
it "preserves an empty title" do
|
||||
expect(described_class.to_html("")).to eq("")
|
||||
end
|
||||
|
||||
it "escapes a title without hashtags" do
|
||||
expect(described_class.to_html(%q(A < B & "C" > D's))).to eq("A < B & "C" > D's")
|
||||
end
|
||||
|
||||
it "links a hashtag at the start of a title" do
|
||||
expect(described_class.to_html("#music live")).to eq(%q(<a href="/search?q=%23music">#music</a> live))
|
||||
end
|
||||
|
||||
it "links multiple hashtags including one at the end" do
|
||||
expect(described_class.to_html("Live #music and #shorts")).to eq(
|
||||
%q(Live <a href="/search?q=%23music">#music</a> and <a href="/search?q=%23shorts">#shorts</a>)
|
||||
)
|
||||
end
|
||||
|
||||
it "keeps punctuation outside hashtag links" do
|
||||
expect(described_class.to_html("(#music), #live!")).to eq(
|
||||
%q((<a href="/search?q=%23music">#music</a>), <a href="/search?q=%23live">#live</a>!)
|
||||
)
|
||||
end
|
||||
|
||||
it "supports numbers and underscores" do
|
||||
expect(described_class.to_html("#2026 #live_music")).to eq(
|
||||
%q(<a href="/search?q=%232026">#2026</a> <a href="/search?q=%23live_music">#live_music</a>)
|
||||
)
|
||||
end
|
||||
|
||||
it "preserves Unicode text around and inside hashtags" do
|
||||
expect(described_class.to_html("音楽 🎵 #日本語 — #موسيقى")).to eq(
|
||||
%q(音楽 🎵 <a href="/search?q=%23%E6%97%A5%E6%9C%AC%E8%AA%9E">#日本語</a> — <a href="/search?q=%23%D9%85%D9%88%D8%B3%D9%8A%D9%82%D9%89">#موسيقى</a>)
|
||||
)
|
||||
end
|
||||
|
||||
it "includes combining marks without normalizing the query" do
|
||||
expect(described_class.to_html("#cafe\u0301")).to eq(
|
||||
"<a href=\"/search?q=%23cafe%CC%81\">#cafe\u0301</a>"
|
||||
)
|
||||
end
|
||||
|
||||
it "preserves whitespace and stops at it" do
|
||||
expect(described_class.to_html("\t#music\n#live ")).to eq(
|
||||
"\t<a href=\"/search?q=%23music\">#music</a>\n<a href=\"/search?q=%23live\">#live</a> "
|
||||
)
|
||||
end
|
||||
|
||||
it "does not link bare hashes, embedded hashes or URL fragments" do
|
||||
title = "# ## C# F#lang word#tag https://example.com/#section https://example.com#other"
|
||||
expect(described_class.to_html(title)).to eq(title)
|
||||
end
|
||||
|
||||
it "does not turn literal or generated HTML entities into links" do
|
||||
expect(described_class.to_html(%q(' ' '))).to eq("' &#39; &#x27;")
|
||||
end
|
||||
|
||||
it "escapes markup and query delimiters next to hashtags" do
|
||||
expect(described_class.to_html(%q(<script>alert('x')</script> #safe&x="bad"))).to eq(
|
||||
%q(<script>alert('x')</script> <a href="/search?q=%23safe">#safe</a>&x="bad")
|
||||
)
|
||||
end
|
||||
end
|
||||
25
src/invidious/frontend/video_title.cr
Normal file
25
src/invidious/frontend/video_title.cr
Normal file
@ -0,0 +1,25 @@
|
||||
require "html"
|
||||
require "uri"
|
||||
|
||||
module Invidious::Frontend::VideoTitle
|
||||
extend self
|
||||
|
||||
# Keep word-internal hashes, URL fragments and HTML entities as plain text.
|
||||
HASHTAG = /(?<![\p{L}\p{M}\p{N}_&#\/])#[\p{L}\p{N}_][\p{L}\p{M}\p{N}_]*/
|
||||
|
||||
def to_html(title : String) : String
|
||||
String.build do |html|
|
||||
offset = 0
|
||||
|
||||
# Match the original text, not escaped entities (which may contain '#').
|
||||
title.scan(HASHTAG) do |match|
|
||||
html << HTML.escape(title.byte_slice(offset, match.byte_begin - offset))
|
||||
html << "<a href=\"/search?q=" << URI.encode_www_form(match[0]) << "\">"
|
||||
html << HTML.escape(match[0]) << "</a>"
|
||||
offset = match.byte_end
|
||||
end
|
||||
|
||||
html << HTML.escape(title.byte_slice(offset, title.bytesize - offset))
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -93,7 +93,7 @@ we're going to need to do it here in order to allow for translations.
|
||||
|
||||
<div class="h-box">
|
||||
<h1>
|
||||
<%= title %>
|
||||
<%= Invidious::Frontend::VideoTitle.to_html(video.title) %>
|
||||
<% if params.listen %>
|
||||
<a title="<%=I18n.translate(locale, "Video mode")%>" id="link-iv-listen" data-base-url="/watch?<%= env.params.query %>&listen=0" href="/watch?<%= env.params.query %>&listen=0">
|
||||
<i class="icon ion-ios-videocam"></i>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user