From 5222cfa1cb59ef65e240053f7a93a35b1e493b97 Mon Sep 17 00:00:00 2001 From: YoursFunny Date: Mon, 21 Sep 2026 17:29:18 +0800 Subject: [PATCH] refactor(x-media): hoist the shared caption builder bilibili and misskey each had a byte-identical caption() (same escaping, same empty-text early return, differing only in a named format argument). site::caption holds it once; both adapters call it. --- crates/x-media/src/site/bilibili/interface.rs | 17 ++--------------- crates/x-media/src/site/misskey/interface.rs | 17 ++--------------- crates/x-media/src/site/mod.rs | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 30 deletions(-) diff --git a/crates/x-media/src/site/bilibili/interface.rs b/crates/x-media/src/site/bilibili/interface.rs index 614a3ba..fe4c69b 100644 --- a/crates/x-media/src/site/bilibili/interface.rs +++ b/crates/x-media/src/site/bilibili/interface.rs @@ -30,7 +30,7 @@ use super::model; use crate::media::Media; use crate::site::{FetchError, Fetched, RenderData, Site, SiteFuture, compose_text}; -use html_escape::{encode_double_quoted_attribute, encode_text}; +use html_escape::encode_text; use regex::Regex; use std::sync::LazyLock; use std::sync::atomic::{AtomicBool, Ordering}; @@ -276,7 +276,7 @@ impl From for Fetched { let text = compose_text(&title, &content); let tags = topic_name(&item).to_string(); - let caption = caption(&url, &author_url, &author, &text); + let caption = crate::site::caption(&url, &author_url, &author, &text); let media = media_of(&item); Fetched { @@ -485,19 +485,6 @@ fn to_https(url: &str) -> String { } } -fn caption(url: &str, author_url: &str, author: &str, text: &str) -> String { - let url = encode_double_quoted_attribute(url); - let author_url = encode_double_quoted_attribute(author_url); - let author = encode_text(author); - if text.is_empty() { - return format!("{url}\n{author}"); - } - format!( - "{url}\n{author}: {}", - encode_text(text) - ) -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/x-media/src/site/misskey/interface.rs b/crates/x-media/src/site/misskey/interface.rs index 4f13a79..458b98f 100644 --- a/crates/x-media/src/site/misskey/interface.rs +++ b/crates/x-media/src/site/misskey/interface.rs @@ -4,7 +4,7 @@ use super::model; use crate::media::Media; use crate::site::{FetchError, Fetched, RenderData, Site, SiteFuture}; -use html_escape::{encode_double_quoted_attribute, encode_text}; +use html_escape::encode_text; use regex::Regex; use std::sync::LazyLock; @@ -117,7 +117,7 @@ impl From for Fetched { text.push_str(content.text.as_deref().unwrap_or_default().trim()); let text = text.trim().to_string(); - let caption = caption(&url, &author_url, &author, &text); + let caption = crate::site::caption(&url, &author_url, &author, &text); let sensitive = content.cw.is_some() || content.files.iter().any(|f| f.is_sensitive); let media: Vec = content.files.iter().filter_map(media_from_file).collect(); @@ -143,19 +143,6 @@ impl From for Fetched { } } -fn caption(url: &str, author_url: &str, author: &str, text: &str) -> String { - let url = encode_double_quoted_attribute(url); - let author_url = encode_double_quoted_attribute(author_url); - let author = encode_text(author); - if text.is_empty() { - return format!("{url}\n{author}"); - } - format!( - "{url}\n{author}: {text}", - text = encode_text(text), - ) -} - /// Maps a Misskey DriveFile to a [`Media`] item; unknown/audio/other types /// are skipped (twitter's `_ => {}` precedent). GIF must be matched before /// the generic image arm. diff --git a/crates/x-media/src/site/mod.rs b/crates/x-media/src/site/mod.rs index e6ce71a..76907f5 100644 --- a/crates/x-media/src/site/mod.rs +++ b/crates/x-media/src/site/mod.rs @@ -79,6 +79,24 @@ pub(crate) struct RenderData { pub tags: String, } +/// The built-in caption for a post that has no user-supplied format: the +/// canonical URL, the author as a link, then the post's text after a colon. +/// The two URLs are escaped for an HTML attribute and the text as HTML text, +/// so site-supplied content cannot inject markup. Shared by the adapters whose +/// captions have exactly this shape (bilibili, misskey). +pub fn caption(url: &str, author_url: &str, author: &str, text: &str) -> String { + let url = html_escape::encode_double_quoted_attribute(url); + let author_url = html_escape::encode_double_quoted_attribute(author_url); + let author = html_escape::encode_text(author); + if text.is_empty() { + return format!("{url}\n{author}"); + } + format!( + "{url}\n{author}: {}", + html_escape::encode_text(text) + ) +} + /// The post's text as one string: title and content joined by a line break, /// each only when it is non-empty. This is what the sites' built-in captions /// show after the author line, and what the bot quotes when it is long.