fix: use UTF-16 offsets for description links

This commit is contained in:
waroxa 2026-08-02 17:57:40 -04:00
parent ad4b1c69ce
commit ed68874dc6
2 changed files with 63 additions and 6 deletions

View File

@ -0,0 +1,56 @@
require "../../spec_helper"
require "../../../src/invidious/videos/description"
Spectator.describe "Video description parser" do
describe "#parse_description" do
it "uses UTF-16 offsets when an emoji precedes a command" do
description = JSON.parse(<<-JSON)
{
"content": "🚀 Visit example now",
"commandRuns": [{
"startIndex": 3,
"length": 5,
"onTap": {
"innertubeCommand": {
"urlEndpoint": {"url": "https://example.com"}
}
}
}]
}
JSON
expect(parse_description(description, "video-id")).to eq(
%(🚀 <a href="https://example.com">Visit</a> example now)
)
end
it "uses UTF-16 lengths when an emoji is inside a command" do
description = JSON.parse(<<-JSON)
{
"content": "Open 😀 docs",
"commandRuns": [{
"startIndex": 0,
"length": 7,
"onTap": {
"innertubeCommand": {
"urlEndpoint": {"url": "https://example.com"}
}
}
}]
}
JSON
expect(parse_description(description, "video-id")).to eq(
%(<a href="https://example.com">Open 😀</a> docs)
)
end
it "escapes complete descriptions without command runs" do
description = JSON.parse(%({"content":"🚀 <safe> & complete"}))
expect(parse_description(description, "video-id")).to eq(
%(🚀 &lt;safe&gt; &amp; complete)
)
end
end
end

View File

@ -1,9 +1,9 @@
require "json"
require "uri"
private def copy_string(str : String::Builder, iter : Iterator, count : Int) : Int
private def copy_string(str : String::Builder, iter : Iterator, count : Int? = nil) : Int
copied = 0
while copied < count
while count.nil? || copied < count
cp = iter.next
break if cp.is_a?(Iterator::Stop)
@ -21,7 +21,9 @@ private def copy_string(str : String::Builder, iter : Iterator, count : Int) : I
str << cp.chr
end
copied += 1
# YouTube expresses command offsets in UTF-16 code units. Astral
# codepoints therefore count as two units instead of one.
copied += cp > 0xFFFF ? 2 : 1
end
return copied
@ -38,7 +40,7 @@ def parse_description(desc, video_id : String) : String?
# Slightly faster than HTML.escape, as we're only doing one pass on
# the string instead of five for the standard library
return String.build do |str|
copy_string(str, content.each_codepoint, content.size)
copy_string(str, content.each_codepoint)
end
end
@ -70,7 +72,6 @@ def parse_description(desc, video_id : String) : String?
end
# Copy the end of the string (past the last command).
remaining_length = content.size - index
copy_string(str, iter, remaining_length) if remaining_length > 0
copy_string(str, iter)
end
end