mirror of
https://github.com/iv-org/invidious.git
synced 2026-09-26 11:35:27 -05:00
Merge ae946c1cb515d9052ed2cc5b54dd2b894e9a86a5 into e4ad826f1fccdd343477020a0122d1bbc397efa9
This commit is contained in:
commit
42c8fb5898
73
spec/invidious/frontend/video_title_spec.cr
Normal file
73
spec/invidious/frontend/video_title_spec.cr
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
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 "links hashtags at the start and end of a title" do
|
||||||
|
expect(described_class.to_html("#music live #concert")).to eq(
|
||||||
|
%(<a href="/hashtag/music">#music</a> live <a href="/hashtag/concert">#concert</a>)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "keeps surrounding punctuation outside links" do
|
||||||
|
expect(described_class.to_html("(#one), [#two]! {#three}. #four-five")).to eq(
|
||||||
|
%((<a href="/hashtag/one">#one</a>), [<a href="/hashtag/two">#two</a>]! {<a href="/hashtag/three">#three</a>}. <a href="/hashtag/four">#four</a>-five)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "preserves case, numbers and underscores" do
|
||||||
|
expect(described_class.to_html("#Open_Source2026 #123")).to eq(
|
||||||
|
%(<a href="/hashtag/Open_Source2026">#Open_Source2026</a> <a href="/hashtag/123">#123</a>)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "supports Unicode letters and combining marks with encoded paths" do
|
||||||
|
expect(described_class.to_html("🎵 #日本語 #cafe\u0301 #हिन्दी")).to eq(
|
||||||
|
%(🎵 <a href="/hashtag/%E6%97%A5%E6%9C%AC%E8%AA%9E">#日本語</a> <a href="/hashtag/cafe%CC%81">#cafe\u0301</a> <a href="/hashtag/%E0%A4%B9%E0%A4%BF%E0%A4%A8%E0%A5%8D%E0%A4%A6%E0%A5%80">#हिन्दी</a>)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "preserves whitespace between consecutive hashtags" do
|
||||||
|
expect(described_class.to_html("#one\t#two\n#three")).to eq(
|
||||||
|
%(<a href="/hashtag/one">#one</a>\t<a href="/hashtag/two">#two</a>\n<a href="/hashtag/three">#three</a>)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "recognizes quoted hashtags and Unicode whitespace" do
|
||||||
|
expect(described_class.to_html(%('#one' "#two"\u00a0#three))).to eq(
|
||||||
|
%('<a href="/hashtag/one">#one</a>' "<a href="/hashtag/two">#two</a>"\u00a0<a href="/hashtag/three">#three</a>)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not link word suffixes, URL fragments or bare hashes" do
|
||||||
|
title = "C# C#Sharp word#tag https://example.com/#fragment https://example.com?q=#section # ##tag #!"
|
||||||
|
expect(described_class.to_html(title)).to eq(title)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "escapes markup and quotes while preserving the displayed title" do
|
||||||
|
expect(described_class.to_html(%(<script>alert("x")</script> #safe & 'quoted'))).to eq(
|
||||||
|
%(<script>alert("x")</script> <a href="/hashtag/safe">#safe</a> & 'quoted')
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not turn escaped character references into hashtags" do
|
||||||
|
expect(described_class.to_html(%(' " { & #ok))).to eq(
|
||||||
|
%(' " &#123; &amp; <a href="/hashtag/ok">#ok</a>)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "escapes attempted attribute injection after a hashtag" do
|
||||||
|
expect(described_class.to_html(%(#tag" onclick="alert(1)))).to eq(
|
||||||
|
%(<a href="/hashtag/tag">#tag</a>" onclick="alert(1))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "handles empty titles and titles without hashtags" do
|
||||||
|
expect(described_class.to_html("")).to eq("")
|
||||||
|
expect(described_class.to_html("Music & video")).to eq("Music & video")
|
||||||
|
end
|
||||||
|
end
|
||||||
28
src/invidious/frontend/video_title.cr
Normal file
28
src/invidious/frontend/video_title.cr
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
require "html"
|
||||||
|
require "uri"
|
||||||
|
|
||||||
|
module Invidious::Frontend::VideoTitle
|
||||||
|
extend self
|
||||||
|
|
||||||
|
# Keep word-internal hashes, URL fragments and HTML entities as plain text.
|
||||||
|
HASHTAG = /(^|[\s\p{Z}(\[{"'])#([\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(0) - offset))
|
||||||
|
html << HTML.escape(match[1])
|
||||||
|
|
||||||
|
tag = match[2]
|
||||||
|
html << %(<a href="/hashtag/) << URI.encode_www_form(tag, space_to_plus: false) << %(">#)
|
||||||
|
html << HTML.escape(tag) << "</a>"
|
||||||
|
offset = match.byte_end(0)
|
||||||
|
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">
|
<div class="h-box">
|
||||||
<h1>
|
<h1>
|
||||||
<%= title %>
|
<%= Invidious::Frontend::VideoTitle.to_html(video.title) %>
|
||||||
<% if params.listen %>
|
<% 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">
|
<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>
|
<i class="icon ion-ios-videocam"></i>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user