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.
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) {
/// 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
}
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");
+ }
+}
.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 {
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>) {
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,
|| 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,
favourites,
mentions,
mentions_header,
+ urls,
+ urls_header,
client_name,
client_url,
sep: SeparatorLine::new(None, false, false),
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));