From fccfc2fadc3f97760aa981b707723ddf4e9fc86c Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Fri, 5 Jan 2024 14:09:59 +0000 Subject: [PATCH] Support listing the URLs in a post. I didn't have to trawl RcDom after all (as my TODO item suggested). All I had to do was to put a mutable reference to a Vec into OurDecorator, and then that could push URLs on to the vector. Or rather: firstly I had to do that using a &RefCell>, because the same Vec reference had to be passed to the separate decorator instance spawned for sub-blocks. Secondly that meant I had to bake a lifetime into the type of OurDecorator (since the call site must know that the reference to the RefCell went away when the config object is dropped). Also, I wanted to exclude the URLs that come from mentions and hashtags, which meant tracking whether a colour annotation had been applied to anything inside the link - whether it was already live at the call to decorate_link_start or was turned on immediately afterwards. --- TODO.md | 14 ----------- src/html.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++++------- src/text.rs | 33 ++++++++++++++++++++++++- 3 files changed, 94 insertions(+), 24 deletions(-) diff --git a/TODO.md b/TODO.md index 748723f..7cf8d7e 100644 --- a/TODO.md +++ b/TODO.md @@ -173,20 +173,6 @@ In real Monochrome, files that have new things to read are marked in the menu by a red +. Perhaps at least the home timeline could usefully be marked that way. -## URLs in Post Info - -Most Mastodon posts repeat the target of a hyperlink in the visible -text, so you can just copy-paste from your terminal into a browser. -But occasional posts (perhaps federated from other styles of -ActivityPub?) do the more webby thing of having the link text and link -target independent. In that situation you want _some_ way of getting -at the URLs. - -An obvious place to list this would be in the [I] post info page. - -(Actually doing this is probably fiddly, and involves trawling the -`RcDom` structure returned from `html2text`'s first phase.) - ## More logs Our logs menus are underpopulated. diff --git a/src/html.rs b/src/html.rs index 73cb40a..30188b4 100644 --- a/src/html.rs +++ b/src/html.rs @@ -3,30 +3,61 @@ pub use html2text::RenderTree; use html2text::render::text_renderer::{ TextDecorator, TaggedLine, TaggedLineElement }; +use std::cell::RefCell; use super::coloured_string::ColouredString; #[derive(Clone, Debug, Default)] -struct OurDecorator { +struct OurDecorator<'a> { + urls: Option<&'a RefCell>>, + colours_pushed: usize, + current_url: Option, } -impl OurDecorator { - fn new() -> OurDecorator { - OurDecorator { } +impl<'a> OurDecorator<'a> { + fn new() -> Self { + Self::with_option_urls(None) + } + + fn with_urls(urls: &'a RefCell>) -> Self { + Self::with_option_urls(Some(urls)) + } + + fn with_option_urls(urls: Option<&'a RefCell>>) -> Self { + OurDecorator { + urls, + colours_pushed: 0, + current_url: None, + } } } -impl TextDecorator for OurDecorator { +impl<'a> TextDecorator for OurDecorator<'a> { type Annotation = char; /// Return an annotation and rendering prefix for a link. - fn decorate_link_start(&mut self, _url: &str) + fn decorate_link_start(&mut self, url: &str) -> (String, Self::Annotation) { + if self.colours_pushed == 0 && self.urls.is_some() { + self.current_url = Some(url.to_owned()); + } ("".to_string(), 'u') } /// Return a suffix for after a link. - fn decorate_link_end(&mut self) -> String { "".to_string() } + fn decorate_link_end(&mut self) -> String { + if let Some(url) = self.current_url.take() { + if let Some(ref rc) = self.urls { + // This is safe because the borrow only lasts for the + // duration of this Vec::push, and it's the only + // borrow_mut of this RefCell anywhere, so nothing is + // going to be trying to re-borrow it during + // re-entrant code. + rc.borrow_mut().push(url); + } + } + "".to_string() + } /// Return an annotation and rendering prefix for em fn decorate_em_start(&mut self) -> (String, Self::Annotation) { @@ -92,20 +123,28 @@ impl TextDecorator for OurDecorator { /// Return a new decorator of the same type which can be used /// for sub blocks. fn make_subblock_decorator(&self) -> Self { - OurDecorator::new() + OurDecorator::with_option_urls(self.urls.clone()) } /// Return an annotation corresponding to adding colour, or none. fn push_colour(&mut self, col: Colour) -> Option { - match col.r { + let annot = match col.r { 1 => Some('@'), 4 => Some('#'), _ => None, + }; + + self.colours_pushed += 1; + if annot.is_some() { + self.current_url = None; } + + annot } /// Pop the last colour pushed if we pushed one. fn pop_colour(&mut self) -> bool { + self.colours_pushed -= 1; true } @@ -183,3 +222,17 @@ fn to_coloured_string(tl: &TaggedLine>) -> ColouredString { pub fn render(rt: &RenderTree, width: usize) -> Vec { render_tl(rt, width).iter().map(to_coloured_string).collect() } + +pub fn list_urls(rt: &RenderTree) -> Vec { + let mut width = 256; + loop { + let urls = RefCell::new(Vec::new()); + if config::with_decorator(OurDecorator::with_urls(&urls)) + .render_to_lines(rt.clone(), width).is_ok() + { + break urls.into_inner(); + } + width = width.checked_mul(2) + .expect("Surely something else went wrong before we got this big"); + } +} diff --git a/src/text.rs b/src/text.rs index 0337d3d..3b9e42f 100644 --- a/src/text.rs +++ b/src/text.rs @@ -758,6 +758,13 @@ impl Html { .map(|line| &prefix + line) .collect() } + + pub fn list_urls(&self) -> Vec { + match self { + Html::Rt(tree) => html::list_urls(tree), + Html::Bad(..) => Vec::new(), + } + } } impl TextFragment for Html { @@ -1784,6 +1791,8 @@ impl StatusDisplay { id: st.id.clone(), } } + + pub fn list_urls(&self) -> Vec { self.content.list_urls() } } fn push_fragment(lines: &mut Vec, frag: Vec) { @@ -1888,6 +1897,8 @@ pub struct DetailedStatusDisplay { favourites: Paragraph, mentions_header: Option, mentions: Vec<(Paragraph, String)>, + urls_header: Option, + urls: Vec, client_name: Paragraph, client_url: Paragraph, sep: SeparatorLine, @@ -1992,8 +2003,20 @@ impl DetailedStatusDisplay { || ColouredString::uniform("none", '0'), |url| ColouredString::uniform(&url, 'u'))); + let sd = StatusDisplay::new(st, client); + let urls: Vec<_> = sd.list_urls().iter().map(|u| { + Paragraph::new().set_indent(2, 4) + .add(&ColouredString::uniform(&u, 'u')) + }).collect(); + let urls_header = if urls.is_empty() { + None + } else { + Some(Paragraph::new().add( + &ColouredString::plain("URLs in hyperlinks:"))) + }; + DetailedStatusDisplay { - sd: StatusDisplay::new(st, client), + sd, id, webstatus, creation, @@ -2011,6 +2034,8 @@ impl DetailedStatusDisplay { favourites, mentions, mentions_header, + urls, + urls_header, client_name, client_url, sep: SeparatorLine::new(None, false, false), @@ -2082,6 +2107,12 @@ impl TextFragment for DetailedStatusDisplay { push_fragment(&mut lines, self.blank.render(width)); } + if !self.urls.is_empty() { + push_fragment(&mut lines, self.urls_header.render(width)); + push_fragment(&mut lines, self.urls.render(width)); + push_fragment(&mut lines, self.blank.render(width)); + } + push_fragment(&mut lines, self.client_name.render(width)); push_fragment(&mut lines, self.client_url.render(width)); push_fragment(&mut lines, self.blank.render(width)); -- 2.30.2