add missed hr tag support, minor reference api updates

This commit is contained in:
yggverse 2026-03-13 17:29:42 +02:00
parent 86ce8ceff5
commit b6b8f96bba
4 changed files with 131 additions and 20 deletions

View file

@ -76,7 +76,7 @@ impl Markdown {
let gutter = Gutter::build(&text_view); let gutter = Gutter::build(&text_view);
// Render markdown tags // 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) // Headers context menu (fragment capture)
let action_header_copy_url = let action_header_copy_url =

View file

@ -1,6 +1,7 @@
mod bold; mod bold;
mod code; mod code;
mod header; mod header;
mod hr;
mod list; mod list;
mod pre; mod pre;
mod quote; mod quote;
@ -11,10 +12,10 @@ mod underline;
use bold::Bold; use bold::Bold;
use code::Code; use code::Code;
use gtk::{ use gtk::{
TextBuffer, TextSearchFlags, TextTag, TextSearchFlags, TextTag, TextView,
gdk::RGBA, gdk::RGBA,
glib::{GString, Uri}, glib::{GString, Uri},
prelude::TextBufferExt, prelude::{TextBufferExt, TextViewExt},
}; };
use header::Header; use header::Header;
use pre::Pre; use pre::Pre;
@ -54,31 +55,32 @@ impl Tags {
} }
pub fn render( pub fn render(
&mut self, &mut self,
buffer: &TextBuffer, text_view: &TextView,
base: &Uri, base: &Uri,
link_color: &RGBA, link_color: &RGBA,
links: &mut HashMap<TextTag, Uri>, links: &mut HashMap<TextTag, Uri>,
headers: &mut HashMap<TextTag, (String, Uri)>, headers: &mut HashMap<TextTag, (String, Uri)>,
) -> Option<String> { ) -> Option<String> {
let buffer = text_view.buffer();
// Collect all code blocks first, // Collect all code blocks first,
// and temporarily replace them with placeholder ID // and temporarily replace them with placeholder ID
self.code.collect(buffer); self.code.collect(&buffer);
// Keep in order! // 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.bold.render(&buffer);
self.pre.render(buffer); self.pre.render(&buffer);
self.strike.render(buffer); self.strike.render(&buffer);
self.underline.render(buffer); self.underline.render(&buffer);
reference::render_images_links(buffer, base, link_color, links); reference::render(&buffer, base, link_color, links);
reference::render_images(buffer, base, link_color, links); hr::render(text_view);
reference::render_links(buffer, base, link_color, links);
// Cleanup unformatted escape chars // Cleanup unformatted escape chars
for e in ESCAPE_ENTRIES { for e in ESCAPE_ENTRIES {
@ -94,11 +96,12 @@ impl Tags {
} }
// Render placeholders // Render placeholders
self.code.render(buffer); self.code.render(&buffer);
// Format document title string // Format document title string
title.map(|mut s| { title.map(|mut s| {
s = bold::strip_tags(&s); s = bold::strip_tags(&s);
s = hr::strip_tags(&s);
s = pre::strip_tags(&s); s = pre::strip_tags(&s);
s = reference::strip_tags(&s); s = reference::strip_tags(&s);
s = strike::strip_tags(&s); s = strike::strip_tags(&s);
@ -118,10 +121,13 @@ pub fn format_header_fragment(value: &str) -> GString {
const ESCAPE_ENTRIES: &[&str] = &[ const ESCAPE_ENTRIES: &[&str] = &[
"\\\n", "\\\\", "\\>", "\\`", "\\!", "\\[", "\\]", "\\(", "\\)", "\\*", "\\#", "\\~", "\\_", "\\\n", "\\\\", "\\>", "\\`", "\\!", "\\[", "\\]", "\\(", "\\)", "\\*", "\\#", "\\~", "\\_",
"\\-",
]; ];
#[test] #[test]
fn test_escape_entries() { fn test_escape_entries() {
let mut set = std::collections::HashSet::new();
for e in ESCAPE_ENTRIES { for e in ESCAPE_ENTRIES {
assert_eq!(e.len(), 2) assert_eq!(e.len(), 2);
assert!(set.insert(*e))
} }
} }

View file

@ -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<hr>\\?[-]{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"], "---");
}

View file

@ -106,7 +106,7 @@ impl Reference {
} }
/// Image links `[![]()]()` /// Image links `[![]()]()`
pub fn render_images_links( fn render_images_links(
buffer: &TextBuffer, buffer: &TextBuffer,
base: &Uri, base: &Uri,
link_color: &RGBA, 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<TextTag, Uri>,
) {
render_images_links(buffer, base, link_color, links);
render_images(buffer, base, link_color, links);
render_links(buffer, base, link_color, links)
}
/// Image tags `![]()` /// Image tags `![]()`
pub fn render_images( fn render_images(
buffer: &TextBuffer, buffer: &TextBuffer,
base: &Uri, base: &Uri,
link_color: &RGBA, link_color: &RGBA,
@ -211,7 +223,7 @@ pub fn render_images(
} }
} }
/// Links `[]()` /// Links `[]()`
pub fn render_links( fn render_links(
buffer: &TextBuffer, buffer: &TextBuffer,
base: &Uri, base: &Uri,
link_color: &RGBA, link_color: &RGBA,