chiark / gitweb /
Support listing the URLs in a post.
authorSimon Tatham <anakin@pobox.com>
Fri, 5 Jan 2024 14:09:59 +0000 (14:09 +0000)
committerSimon Tatham <anakin@pobox.com>
Fri, 5 Jan 2024 14:57:11 +0000 (14:57 +0000)
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<String> into
OurDecorator, and then that could push URLs on to the vector.

Or rather: firstly I had to do that using a &RefCell<Vec<String>>,
because the same Vec<String> 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
src/html.rs
src/text.rs

diff --git a/TODO.md b/TODO.md
index 748723f2c64f6f6ebf049eb2dd2ad43693019963..7cf8d7e8754443684975a29276f848d4d62d77a7 100644 (file)
--- 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.
index 73cb40a560dda4fdab8984be559c5b27f2c661ea..30188b4f1d3a7ada6b998b443cace16232d5291e 100644 (file)
@@ -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<Vec<String>>>,
+    colours_pushed: usize,
+    current_url: Option<String>,
 }
 
-impl OurDecorator {
-    fn new() -> OurDecorator {
-        OurDecorator { }
+impl<'a> OurDecorator<'a> {
+    fn new() -> Self {
+        Self::with_option_urls(None)
+    }
+
+    fn with_urls(urls: &'a RefCell<Vec<String>>) -> Self {
+        Self::with_option_urls(Some(urls))
+    }
+
+    fn with_option_urls(urls: Option<&'a RefCell<Vec<String>>>) -> 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<Self::Annotation> {
-        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<Vec<char>>) -> ColouredString {
 pub fn render(rt: &RenderTree, width: usize) -> Vec<ColouredString> {
     render_tl(rt, width).iter().map(to_coloured_string).collect()
 }
+
+pub fn list_urls(rt: &RenderTree) -> Vec<String> {
+    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");
+    }
+}
index 0337d3dcba4bcae2b2c4561f449a2c1a1daaf595..3b9e42f0123ccde912ebfb6d4a8a69970d264f50 100644 (file)
@@ -758,6 +758,13 @@ impl Html {
             .map(|line| &prefix + line)
             .collect()
     }
+
+    pub fn list_urls(&self) -> Vec<String> {
+        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<String> { self.content.list_urls() }
 }
 
 fn push_fragment(lines: &mut Vec<ColouredString>, frag: Vec<ColouredString>) {
@@ -1888,6 +1897,8 @@ pub struct DetailedStatusDisplay {
     favourites: Paragraph,
     mentions_header: Option<Paragraph>,
     mentions: Vec<(Paragraph, String)>,
+    urls_header: Option<Paragraph>,
+    urls: Vec<Paragraph>,
     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));