From ff8aa06f84c150c072e14841e549cfb77fe85d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8chico10117=C2=A8?= Date: Fri, 7 Aug 2026 16:13:00 -0600 Subject: [PATCH] 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. --- spec/invidious/videos/description_spec.cr | 51 +++++++++++++++++++++++ src/invidious/videos/description.cr | 51 +++++++++++++++++++---- 2 files changed, 94 insertions(+), 8 deletions(-) diff --git a/spec/invidious/videos/description_spec.cr b/spec/invidious/videos/description_spec.cr index b1e2218f9..94173a738 100644 --- a/spec/invidious/videos/description_spec.cr +++ b/spec/invidious/videos/description_spec.cr @@ -96,4 +96,55 @@ Spectator.describe "parse_description" do expect(parse_description(description, "video-id")).to eq("one:custom-emoji:two") 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( + %() + + %(emoji-at-start) + + "ab" + + %(emoji-inside) + + "cd" + ) + end end diff --git a/src/invidious/videos/description.cr b/src/invidious/videos/description.cr index 44979b468..a5d781fe1 100644 --- a/src/invidious/videos/description.cr +++ b/src/invidious/videos/description.cr @@ -93,6 +93,44 @@ private def parse_attachment_run(attachment) : String? 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 runs = [] of Tuple(Int32, Int32, String, JSON::Any) @@ -101,6 +139,8 @@ private def parse_description_with_attachments(content : String, commands, attac end 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} end @@ -122,15 +162,10 @@ private def parse_description_with_attachments(content : String, commands, attac case run[2] when "command" command = run[3] - command_content = String.build(length) do |command_str| - copy_string(command_str, iter, length) + nested_attachments = attachments.select do |attachment| + attachment_run_inside_command?(attachment, command) end - - link = command_content - if on_tap = command.dig?("onTap", "innertubeCommand") - link = parse_link_endpoint(on_tap, command_content, video_id) - end - str << link + str << parse_command_run(command, nested_attachments, iter, video_id) when "attachment" attachment = run[3] if attachment_html = parse_attachment_run(attachment)