From b6b8f96bba55c9287507f8127e7ec51c51177be4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 13 Mar 2026 17:29:42 +0200 Subject: [PATCH] add missed hr tag support, minor reference api updates --- .../tab/item/page/content/text/markdown.rs | 2 +- .../item/page/content/text/markdown/tags.rs | 38 ++++---- .../page/content/text/markdown/tags/hr.rs | 93 +++++++++++++++++++ .../content/text/markdown/tags/reference.rs | 18 +++- 4 files changed, 131 insertions(+), 20 deletions(-) create mode 100644 src/app/browser/window/tab/item/page/content/text/markdown/tags/hr.rs diff --git a/src/app/browser/window/tab/item/page/content/text/markdown.rs b/src/app/browser/window/tab/item/page/content/text/markdown.rs index 5f11f788..49a69990 100644 --- a/src/app/browser/window/tab/item/page/content/text/markdown.rs +++ b/src/app/browser/window/tab/item/page/content/text/markdown.rs @@ -76,7 +76,7 @@ impl Markdown { let gutter = Gutter::build(&text_view); // Render markdown tags - let title = tags.render(&buffer, base, &link_color.0, &mut links, &mut headers); + let title = tags.render(&text_view, base, &link_color.0, &mut links, &mut headers); // Headers context menu (fragment capture) let action_header_copy_url = diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags.rs index 15cb3354..5085b91d 100644 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags.rs +++ b/src/app/browser/window/tab/item/page/content/text/markdown/tags.rs @@ -1,6 +1,7 @@ mod bold; mod code; mod header; +mod hr; mod list; mod pre; mod quote; @@ -11,10 +12,10 @@ mod underline; use bold::Bold; use code::Code; use gtk::{ - TextBuffer, TextSearchFlags, TextTag, + TextSearchFlags, TextTag, TextView, gdk::RGBA, glib::{GString, Uri}, - prelude::TextBufferExt, + prelude::{TextBufferExt, TextViewExt}, }; use header::Header; use pre::Pre; @@ -54,31 +55,32 @@ impl Tags { } pub fn render( &mut self, - buffer: &TextBuffer, + text_view: &TextView, base: &Uri, link_color: &RGBA, links: &mut HashMap, headers: &mut HashMap, ) -> Option { + let buffer = text_view.buffer(); + // Collect all code blocks first, // and temporarily replace them with placeholder ID - self.code.collect(buffer); + self.code.collect(&buffer); // Keep in order! - let title = self.header.render(buffer, base, headers); + let title = self.header.render(&buffer, base, headers); - list::render(buffer); + list::render(&buffer); - self.quote.render(buffer); + self.quote.render(&buffer); - self.bold.render(buffer); - self.pre.render(buffer); - self.strike.render(buffer); - self.underline.render(buffer); + self.bold.render(&buffer); + self.pre.render(&buffer); + self.strike.render(&buffer); + self.underline.render(&buffer); - reference::render_images_links(buffer, base, link_color, links); - reference::render_images(buffer, base, link_color, links); - reference::render_links(buffer, base, link_color, links); + reference::render(&buffer, base, link_color, links); + hr::render(text_view); // Cleanup unformatted escape chars for e in ESCAPE_ENTRIES { @@ -94,11 +96,12 @@ impl Tags { } // Render placeholders - self.code.render(buffer); + self.code.render(&buffer); // Format document title string title.map(|mut s| { s = bold::strip_tags(&s); + s = hr::strip_tags(&s); s = pre::strip_tags(&s); s = reference::strip_tags(&s); s = strike::strip_tags(&s); @@ -118,10 +121,13 @@ pub fn format_header_fragment(value: &str) -> GString { const ESCAPE_ENTRIES: &[&str] = &[ "\\\n", "\\\\", "\\>", "\\`", "\\!", "\\[", "\\]", "\\(", "\\)", "\\*", "\\#", "\\~", "\\_", + "\\-", ]; #[test] fn test_escape_entries() { + let mut set = std::collections::HashSet::new(); for e in ESCAPE_ENTRIES { - assert_eq!(e.len(), 2) + assert_eq!(e.len(), 2); + assert!(set.insert(*e)) } } diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/hr.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/hr.rs new file mode 100644 index 00000000..8cfcc683 --- /dev/null +++ b/src/app/browser/window/tab/item/page/content/text/markdown/tags/hr.rs @@ -0,0 +1,93 @@ +use gtk::{ + Orientation, Separator, TextView, + glib::{ControlFlow, idle_add_local}, + prelude::*, +}; +use regex::Regex; + +const REGEX_HR: &str = r"(?m)^(?P
\\?[-]{3,})$"; + +/// Apply --- `Tag` to given `TextBuffer` +pub fn render(text_view: &TextView) { + let separator = Separator::builder() + .orientation(Orientation::Horizontal) + .build(); + idle_add_local({ + let text_view = text_view.clone(); + let separator = separator.clone(); + move || { + separator.set_width_request(text_view.width() - 18); + ControlFlow::Break + } + }); + + let buffer = text_view.buffer(); + + let (start, end) = buffer.bounds(); + let full_content = buffer.text(&start, &end, true).to_string(); + + let matches: Vec<_> = Regex::new(REGEX_HR) + .unwrap() + .captures_iter(&full_content) + .collect(); + + for cap in matches.into_iter().rev() { + let full_match = cap.get(0).unwrap(); + + let start_char_offset = full_content[..full_match.start()].chars().count() as i32; + let end_char_offset = full_content[..full_match.end()].chars().count() as i32; + + let mut start_iter = buffer.iter_at_offset(start_char_offset); + let mut end_iter = buffer.iter_at_offset(end_char_offset); + + if start_char_offset > 0 + && buffer + .text( + &buffer.iter_at_offset(start_char_offset - 1), + &end_iter, + false, + ) + .contains("\\") + { + continue; + } + + buffer.delete(&mut start_iter, &mut end_iter); + text_view.add_child_at_anchor(&separator, &buffer.create_child_anchor(&mut end_iter)); + } +} + +pub fn strip_tags(value: &str) -> String { + let mut result = String::from(value); + for cap in Regex::new(REGEX_HR).unwrap().captures_iter(value) { + if let Some(m) = cap.get(0) { + result = result.replace(m.as_str(), &cap["hr"]); + } + } + result +} + +#[test] +fn test_strip_tags() { + const VALUE: &str = "Some line\n---\nSome another-line with ![img](https://link.com)"; + let mut result = String::from(VALUE); + for cap in Regex::new(REGEX_HR).unwrap().captures_iter(VALUE) { + if let Some(m) = cap.get(0) { + result = result.replace(m.as_str(), ""); + } + } + assert_eq!( + result, + "Some line\n\nSome another-line with ![img](https://link.com)" + ) +} + +#[test] +fn test_regex() { + let cap: Vec<_> = Regex::new(REGEX_HR) + .unwrap() + .captures_iter("Some line\n---\nSome another-line with ![img](https://link.com)") + .collect(); + + assert_eq!(&cap.first().unwrap()["hr"], "---"); +} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/reference.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/reference.rs index b69622b1..0ce45980 100644 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/reference.rs +++ b/src/app/browser/window/tab/item/page/content/text/markdown/tags/reference.rs @@ -106,7 +106,7 @@ impl Reference { } /// Image links `[![]()]()` -pub fn render_images_links( +fn render_images_links( buffer: &TextBuffer, base: &Uri, link_color: &RGBA, @@ -159,8 +159,20 @@ pub fn render_images_links( } } } + +pub fn render( + buffer: &TextBuffer, + base: &Uri, + link_color: &RGBA, + links: &mut HashMap, +) { + render_images_links(buffer, base, link_color, links); + render_images(buffer, base, link_color, links); + render_links(buffer, base, link_color, links) +} + /// Image tags `![]()` -pub fn render_images( +fn render_images( buffer: &TextBuffer, base: &Uri, link_color: &RGBA, @@ -211,7 +223,7 @@ pub fn render_images( } } /// Links `[]()` -pub fn render_links( +fn render_links( buffer: &TextBuffer, base: &Uri, link_color: &RGBA,