fix(comments): render nested attachment runs

Render attachment runs contained by command runs while consuming the source cursor in one pass. Cover equal-offset and nested zero-length attachments so link processing no longer drops images.
This commit is contained in:
¨chico10117¨ 2026-08-07 16:13:00 -06:00
parent a570e0b43e
commit ff8aa06f84
2 changed files with 94 additions and 8 deletions

View File

@ -96,4 +96,55 @@ Spectator.describe "parse_description" do
expect(parse_description(description, "video-id")).to eq("one:custom-emoji:two") expect(parse_description(description, "video-id")).to eq("one:custom-emoji:two")
end end
it "renders attachments nested in command runs" do
description = JSON.parse(<<-JSON)
{
"content": "abcd",
"commandRuns": [{
"startIndex": 0,
"length": 4,
"onTap": {"innertubeCommand": {"watchEndpoint": {"videoId": "linked-video"}}}
}],
"attachmentRuns": [
{
"startIndex": 0,
"length": 0,
"element": {
"type": {
"imageType": {
"image": {
"sources": [{"url": "https://lh3.googleusercontent.com/emoji-start=s16", "width": 16, "height": 16}]
}
}
}
},
"properties": {"accessibilityProperties": {"label": "emoji-at-start"}}
},
{
"startIndex": 2,
"length": 0,
"element": {
"type": {
"imageType": {
"image": {
"sources": [{"url": "https://lh3.googleusercontent.com/emoji-inside=s16", "width": 16, "height": 16}]
}
}
}
},
"properties": {"accessibilityProperties": {"label": "emoji-inside"}}
}
]
}
JSON
expect(parse_description(description, "video-id")).to eq(
%(<a href="/watch?v=linked-video">) +
%(<img alt="emoji-at-start" src="/ggpht/emoji-start=s16" title="emoji-at-start" width="16" height="16" class="channel-emoji" />) +
"ab" +
%(<img alt="emoji-inside" src="/ggpht/emoji-inside=s16" title="emoji-inside" width="16" height="16" class="channel-emoji" />) +
"cd</a>"
)
end
end end

View File

@ -93,6 +93,44 @@ private def parse_attachment_run(attachment) : String?
end end
end end
private def attachment_run_inside_command?(attachment, command) : Bool
attachment_start = attachment["startIndex"].as_i
attachment_end = attachment_start + attachment["length"].as_i
command_start = command["startIndex"].as_i
command_end = command_start + command["length"].as_i
attachment_start >= command_start && attachment_end <= command_end
end
private def parse_command_run(command, nested_attachments, iter, video_id : String) : String
command_start = command["startIndex"].as_i
command_length = command["length"].as_i
command_content = String.build do |command_str|
index = 0
nested_attachments.sort_by { |attachment| attachment["startIndex"].as_i }.each do |attachment|
relative_start = attachment["startIndex"].as_i - command_start
next if relative_start < index
index += copy_string(command_str, iter, relative_start - index)
if attachment_html = parse_attachment_run(attachment)
command_str << attachment_html
index += skip_string(iter, attachment["length"].as_i)
else
index += copy_string(command_str, iter, attachment["length"].as_i)
end
end
copy_string(command_str, iter, command_length - index) if index < command_length
end
link = command_content
if on_tap = command.dig?("onTap", "innertubeCommand")
link = parse_link_endpoint(on_tap, command_content, video_id)
end
link
end
private def parse_description_with_attachments(content : String, commands, attachments, video_id : String) : String private def parse_description_with_attachments(content : String, commands, attachments, video_id : String) : String
runs = [] of Tuple(Int32, Int32, String, JSON::Any) runs = [] of Tuple(Int32, Int32, String, JSON::Any)
@ -101,6 +139,8 @@ private def parse_description_with_attachments(content : String, commands, attac
end end
attachments.each do |attachment| attachments.each do |attachment|
next if commands.try &.any? { |command| attachment_run_inside_command?(attachment, command) }
runs << {attachment["startIndex"].as_i, attachment["length"].as_i, "attachment", attachment} runs << {attachment["startIndex"].as_i, attachment["length"].as_i, "attachment", attachment}
end end
@ -122,15 +162,10 @@ private def parse_description_with_attachments(content : String, commands, attac
case run[2] case run[2]
when "command" when "command"
command = run[3] command = run[3]
command_content = String.build(length) do |command_str| nested_attachments = attachments.select do |attachment|
copy_string(command_str, iter, length) attachment_run_inside_command?(attachment, command)
end end
str << parse_command_run(command, nested_attachments, iter, video_id)
link = command_content
if on_tap = command.dig?("onTap", "innertubeCommand")
link = parse_link_endpoint(on_tap, command_content, video_id)
end
str << link
when "attachment" when "attachment"
attachment = run[3] attachment = run[3]
if attachment_html = parse_attachment_run(attachment) if attachment_html = parse_attachment_run(attachment)