add underline tag support

This commit is contained in:
yggverse 2026-03-09 06:09:47 +02:00
parent ea2f4656a0
commit 5b8a469b5b
2 changed files with 93 additions and 0 deletions

View file

@ -4,6 +4,7 @@ mod list;
mod quote; mod quote;
mod reference; mod reference;
mod title; mod title;
mod underline;
use std::collections::HashMap; use std::collections::HashMap;
@ -18,6 +19,7 @@ use header::Header;
use list::List; use list::List;
use quote::Quote; use quote::Quote;
use title::Title; use title::Title;
use underline::Underline;
pub struct Tags { pub struct Tags {
pub text_tag_table: TextTagTable, pub text_tag_table: TextTagTable,
@ -27,6 +29,7 @@ pub struct Tags {
pub list: TextTag, pub list: TextTag,
pub quote: Quote, pub quote: Quote,
pub title: TextTag, pub title: TextTag,
pub underline: Underline,
} }
impl Default for Tags { impl Default for Tags {
@ -55,6 +58,7 @@ impl Tags {
list, list,
quote: Quote::new(), quote: Quote::new(),
title, title,
underline: Underline::new(),
} }
} }
pub fn render( pub fn render(
@ -70,6 +74,7 @@ impl Tags {
self.quote.render(buffer); self.quote.render(buffer);
self.bold.render(buffer); self.bold.render(buffer);
self.underline.render(buffer);
reference::render_images_links(&buffer, base, &link_color, links); reference::render_images_links(&buffer, base, &link_color, links);
reference::render_images(&buffer, base, &link_color, links); reference::render_images(&buffer, base, &link_color, links);
@ -78,6 +83,7 @@ impl Tags {
title.map(|mut s| { title.map(|mut s| {
s = reference::strip_tags(&s); s = reference::strip_tags(&s);
s = bold::strip_tags(&s); s = bold::strip_tags(&s);
s = underline::strip_tags(&s);
s // @TODO other tags s // @TODO other tags
}) })
} }

View file

@ -0,0 +1,87 @@
use gtk::{
TextBuffer, TextTag,
WrapMode::Word,
pango::Underline::Single,
prelude::{TextBufferExt, TextBufferExtManual},
};
use regex::Regex;
const REGEX_UNDERLINE: &str = r"\b_(?P<text>[^_]+)_\b";
pub struct Underline(TextTag);
impl Underline {
pub fn new() -> Self {
Self(TextTag::builder().underline(Single).wrap_mode(Word).build())
}
/// Apply _underline_ `Tag` to given `TextBuffer`
pub fn render(&self, buffer: &TextBuffer) {
assert!(buffer.tag_table().add(&self.0));
let (start, end) = buffer.bounds();
let full_content = buffer.text(&start, &end, true).to_string();
let matches: Vec<_> = Regex::new(REGEX_UNDERLINE)
.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);
buffer.delete(&mut start_iter, &mut end_iter);
buffer.insert_with_tags(&mut start_iter, &cap["text"], &[&self.0])
}
}
}
pub fn strip_tags(value: &str) -> String {
let mut result = String::from(value);
for cap in Regex::new(REGEX_UNDERLINE)
.unwrap()
.captures_iter(&value)
.into_iter()
{
if let Some(m) = cap.get(0) {
result = result.replace(m.as_str(), &cap["text"]);
}
}
result
}
#[test]
fn test_strip_tags() {
const VALUE: &str = r"Some _underline 1_ and _underline 2_ with ![img](https://link.com)";
let mut result = String::from(VALUE);
for cap in Regex::new(REGEX_UNDERLINE)
.unwrap()
.captures_iter(VALUE)
.into_iter()
{
if let Some(m) = cap.get(0) {
result = result.replace(m.as_str(), &cap["text"]);
}
}
assert_eq!(
result,
"Some underline 1 and underline 2 with ![img](https://link.com)"
)
}
#[test]
fn test_regex() {
let cap: Vec<_> = Regex::new(REGEX_UNDERLINE)
.unwrap()
.captures_iter(r"Some _underline 1_ and _underline 2_ with ![img](https://link.com)")
.collect();
assert_eq!(&cap.get(0).unwrap()["text"], "underline 1");
assert_eq!(&cap.get(1).unwrap()["text"], "underline 2");
}