mirror of
https://github.com/iv-org/invidious.git
synced 2026-08-17 05:20:51 -05:00
feat: render custom channel emoji in descriptions and comments
Add attachmentRuns handling to parse_description so custom channel emoji (image shortcodes such as :yt:) render as <img> tags instead of falling back to their textual form. The commentViewModel path (comments/youtube.cr) and the video description path (parser.cr) both route through parse_description, which previously only handled commandRuns (links). This change merges commandRuns and attachmentRuns sorted by startIndex and emits an <img class=channel-emoji> for image-type attachments, mirroring the behaviour of the older content_to_comment_html implementation. Fixes #5888
This commit is contained in:
parent
eb407578a2
commit
bab6a7b798
@ -40,6 +40,8 @@ private def copy_string(str : String::Builder, iter : Iterator, count : Int) : I
|
|||||||
return copied
|
return copied
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private record Run, start_index : Int32, length : Int32, kind : Symbol, data : JSON::Any
|
||||||
|
|
||||||
def parse_description(desc, video_id : String) : String?
|
def parse_description(desc, video_id : String) : String?
|
||||||
return "" if desc.nil?
|
return "" if desc.nil?
|
||||||
|
|
||||||
@ -47,47 +49,119 @@ def parse_description(desc, video_id : String) : String?
|
|||||||
return "" if content.empty?
|
return "" if content.empty?
|
||||||
|
|
||||||
commands = desc["commandRuns"]?.try &.as_a
|
commands = desc["commandRuns"]?.try &.as_a
|
||||||
if commands.nil?
|
attachments = desc["attachmentRuns"]?.try &.as_a
|
||||||
# Slightly faster than HTML.escape, as we're only doing one pass on
|
|
||||||
# the string instead of five for the standard library
|
# If no runs at all, fall back to simple escape
|
||||||
|
if commands.nil? && attachments.nil?
|
||||||
return String.build do |str|
|
return String.build do |str|
|
||||||
content_size = content.ascii_only? ? content.size : utf16_length(content)
|
content_size = content.ascii_only? ? content.size : utf16_length(content)
|
||||||
copy_string(str, content.each_codepoint, content_size)
|
copy_string(str, content.each_codepoint, content_size)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Not everything is stored in UTF-8 on youtube's side. The SMP codepoints
|
# Merge commandRuns and attachmentRuns sorted by startIndex
|
||||||
# (0x10000 and above) are encoded as UTF-16 surrogate pairs, which are
|
runs = [] of Run
|
||||||
# automatically decoded by the JSON parser. It means that we need to count
|
|
||||||
# copied byte in a special manner, preventing the use of regular string copy.
|
|
||||||
iter = content.each_codepoint
|
|
||||||
|
|
||||||
|
if commands
|
||||||
|
commands.each do |cmd|
|
||||||
|
runs << Run.new(
|
||||||
|
start_index: cmd["startIndex"].as_i,
|
||||||
|
length: cmd["length"].as_i,
|
||||||
|
kind: :command,
|
||||||
|
data: cmd
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if attachments
|
||||||
|
attachments.each do |att|
|
||||||
|
runs << Run.new(
|
||||||
|
start_index: att["startIndex"].as_i,
|
||||||
|
length: att["length"].as_i,
|
||||||
|
kind: :attachment,
|
||||||
|
data: att
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
runs.sort_by!(&.start_index)
|
||||||
|
|
||||||
|
iter = content.each_codepoint
|
||||||
index = 0
|
index = 0
|
||||||
|
|
||||||
return String.build do |str|
|
return String.build do |str|
|
||||||
commands.each do |command|
|
runs.each do |run|
|
||||||
cmd_start = command["startIndex"].as_i
|
cmd_start = run.start_index
|
||||||
cmd_length = command["length"].as_i
|
cmd_length = run.length
|
||||||
|
|
||||||
# Copy the text chunk between this command and the previous if needed.
|
# Copy the text chunk between this run and the previous if needed.
|
||||||
length = cmd_start - index
|
length = cmd_start - index
|
||||||
index += copy_string(str, iter, length)
|
index += copy_string(str, iter, length)
|
||||||
|
|
||||||
# We need to copy the command's text using the iterator
|
# We need to copy the run's text using the iterator
|
||||||
# and the special function defined above.
|
# and the special function defined above.
|
||||||
cmd_content = String.build(cmd_length) do |str2|
|
cmd_content = String.build(cmd_length) do |str2|
|
||||||
copy_string(str2, iter, cmd_length)
|
copy_string(str2, iter, cmd_length)
|
||||||
end
|
end
|
||||||
|
|
||||||
link = cmd_content
|
if run.kind == :command
|
||||||
if on_tap = command.dig?("onTap", "innertubeCommand")
|
# Handle link commands
|
||||||
link = parse_link_endpoint(on_tap, cmd_content, video_id)
|
link = cmd_content
|
||||||
|
if on_tap = run.data.dig?("onTap", "innertubeCommand")
|
||||||
|
link = parse_link_endpoint(on_tap, cmd_content, video_id)
|
||||||
|
end
|
||||||
|
str << link
|
||||||
|
elsif run.kind == :attachment
|
||||||
|
# Handle attachment runs (emoji images)
|
||||||
|
element = run.data["element"]?
|
||||||
|
|
||||||
|
if element.is_a?(JSON::Any) && element["type"]?.try &.as_s == "imageType"
|
||||||
|
image = element["image"]?
|
||||||
|
|
||||||
|
if image.is_a?(JSON::Any)
|
||||||
|
sources = image["sources"]?.try &.as_a
|
||||||
|
|
||||||
|
if sources && sources[0]?
|
||||||
|
source = sources[0]
|
||||||
|
# Get the emoji image URL
|
||||||
|
emoji_url = source["url"]?.try &.as_s
|
||||||
|
# Fallback for clientResource format (badges)
|
||||||
|
if emoji_url.nil? || emoji_url.empty?
|
||||||
|
emoji_url = source.dig?("clientResource", "imageName").try &.as_s
|
||||||
|
end
|
||||||
|
|
||||||
|
if emoji_url && !emoji_url.empty?
|
||||||
|
# Get accessibility label
|
||||||
|
alt_text = image.dig?("accessibility", "accessibilityData", "label").try &.as_s || cmd_content
|
||||||
|
|
||||||
|
# Build the img tag
|
||||||
|
if emoji_url.starts_with?("http://") || emoji_url.starts_with?("https://")
|
||||||
|
str << %(<img alt=") << alt_text << "\" "
|
||||||
|
str << %(src=") << emoji_url << "\" "
|
||||||
|
str << %(title=") << alt_text << "\" "
|
||||||
|
str << %(class="channel-emoji" />)
|
||||||
|
else
|
||||||
|
# For internal resource names (badges), skip rendering
|
||||||
|
str << cmd_content
|
||||||
|
end
|
||||||
|
else
|
||||||
|
str << cmd_content
|
||||||
|
end
|
||||||
|
else
|
||||||
|
str << cmd_content
|
||||||
|
end
|
||||||
|
else
|
||||||
|
str << cmd_content
|
||||||
|
end
|
||||||
|
else
|
||||||
|
str << cmd_content
|
||||||
|
end
|
||||||
end
|
end
|
||||||
str << link
|
|
||||||
index += cmd_length
|
index += cmd_length
|
||||||
end
|
end
|
||||||
|
|
||||||
# Copy the end of the string (past the last command).
|
# Copy the end of the string (past the last run).
|
||||||
content_size = content.ascii_only? ? content.size : utf16_length(content)
|
content_size = content.ascii_only? ? content.size : utf16_length(content)
|
||||||
remaining_length = content_size - index
|
remaining_length = content_size - index
|
||||||
copy_string(str, iter, remaining_length) if remaining_length > 0
|
copy_string(str, iter, remaining_length) if remaining_length > 0
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user