diff --git a/spec/invidious/helpers_spec.cr b/spec/invidious/helpers_spec.cr index 9fbb6d6fe..6908bf7c3 100644 --- a/spec/invidious/helpers_spec.cr +++ b/spec/invidious/helpers_spec.cr @@ -3,6 +3,36 @@ require "../spec_helper" CONFIG = Config.from_yaml(File.open("config/config.example.yml")) Spectator.describe "Helper" do + describe "#parse_video_title" do + it "links hashtags in video titles" do + expect(parse_video_title("Video title #music")) + .to eq(%(Video title #music)) + end + + it "links hashtags after punctuation" do + expect(parse_video_title("Watch: #music")) + .to eq(%(Watch: #music)) + end + + it "does not link hashes inside words" do + expect(parse_video_title("Video title test#music")) + .to eq("Video title test#music") + end + + it "encodes unicode hashtags" do + hashtag = "\u{4E2D}\u{6587}" + punctuation = "\u{FF0C}" + + expect(parse_video_title("Video ##{hashtag}#{punctuation}")) + .to eq(%(Video ##{hashtag}#{punctuation})) + end + + it "escapes video titles before linking hashtags" do + expect(parse_video_title(%(#music & #news))) + .to eq(%(<b>#music</b> & #news)) + end + end + describe "#produce_channel_search_continuation" do it "correctly produces token for searching a specific channel" do expect(produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100)).to eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D") diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 7a262f849..69efd1bf3 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -20,6 +20,50 @@ def elapsed_text(elapsed) "#{(millis * 1000).round(2)}µs" end +def parse_video_title(title : String) : String + title_chars = title.chars + + return String.build(title.size) do |str| + index = 0 + + while index < title_chars.size + char = title_chars[index] + + if char == '#' && video_title_hashtag_start?(title_chars, index) + hashtag_end = index + 1 + + while hashtag_end < title_chars.size && video_title_hashtag_char?(title_chars[hashtag_end]) + hashtag_end += 1 + end + + hashtag = title_chars[(index + 1)...hashtag_end].join + url = URI.encode_path("/hashtag/#{hashtag}") + + str << %() + str << HTML.escape("##{hashtag}") + str << "" + + index = hashtag_end + else + str << HTML.escape(char.to_s) + index += 1 + end + end + end +end + +private def video_title_hashtag_start?(title_chars : Array(Char), index : Int) : Bool + next_char = title_chars[index + 1]? + return false if next_char.nil? + return false unless video_title_hashtag_char?(next_char) + + index == 0 || !video_title_hashtag_char?(title_chars[index - 1]) +end + +private def video_title_hashtag_char?(char : Char) : Bool + char == '_' || char.alphanumeric? +end + def decode_length_seconds(string) length_seconds = string.gsub(/[^0-9:]/, "") return 0_i32 if length_seconds.empty? diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 796c0c914..ee393e7de 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -1,5 +1,6 @@ <% ucid = video.ucid %> <% title = HTML.escape(video.title) %> +<% title_html = parse_video_title(video.title) %> <% author = HTML.escape(video.author) %> @@ -77,7 +78,7 @@ we're going to need to do it here in order to allow for translations.

- <%= title %> + <%= title_html %> <% if params.listen %> " id="link-iv-listen" data-base-url="/watch?<%= env.params.query %>&listen=0" href="/watch?<%= env.params.query %>&listen=0">