draft basic multi-line code tags impl

This commit is contained in:
yggverse 2026-03-09 17:46:02 +02:00
parent d674edc7d0
commit a8d25e695f
3 changed files with 119 additions and 4 deletions

View file

@ -6,7 +6,6 @@ mod tags;
use super::{ItemAction, WindowAction}; use super::{ItemAction, WindowAction};
use crate::app::browser::window::action::Position; use crate::app::browser::window::action::Position;
pub use error::Error;
use gtk::{ use gtk::{
EventControllerMotion, GestureClick, TextBuffer, TextTag, TextTagTable, TextView, EventControllerMotion, GestureClick, TextBuffer, TextTag, TextTagTable, TextView,
TextWindowType, UriLauncher, Window, WrapMode, TextWindowType, UriLauncher, Window, WrapMode,
@ -56,7 +55,7 @@ impl Markdown {
let syntax = Syntax::new(); let syntax = Syntax::new();
// Init tags // Init tags
let tags = Tags::new(); let mut tags = Tags::new();
// Init new text buffer // Init new text buffer
let buffer = TextBuffer::new(Some(&TextTagTable::new())); let buffer = TextBuffer::new(Some(&TextTagTable::new()));

View file

@ -1,5 +1,6 @@
mod bold; mod bold;
mod header; mod header;
mod pre;
mod quote; mod quote;
mod reference; mod reference;
mod strike; mod strike;
@ -10,6 +11,7 @@ use std::collections::HashMap;
use bold::Bold; use bold::Bold;
use gtk::{TextBuffer, TextTag, gdk::RGBA, glib::Uri}; use gtk::{TextBuffer, TextTag, gdk::RGBA, glib::Uri};
use header::Header; use header::Header;
use pre::Pre;
use quote::Quote; use quote::Quote;
use strike::Strike; use strike::Strike;
use underline::Underline; use underline::Underline;
@ -17,6 +19,7 @@ use underline::Underline;
pub struct Tags { pub struct Tags {
pub bold: Bold, pub bold: Bold,
pub header: Header, pub header: Header,
pub pre: Pre,
pub quote: Quote, pub quote: Quote,
pub strike: Strike, pub strike: Strike,
pub underline: Underline, pub underline: Underline,
@ -34,19 +37,23 @@ impl Tags {
Self { Self {
bold: Bold::new(), bold: Bold::new(),
header: Header::new(), header: Header::new(),
pre: Pre::new(),
quote: Quote::new(), quote: Quote::new(),
strike: Strike::new(), strike: Strike::new(),
underline: Underline::new(), underline: Underline::new(),
} }
} }
pub fn render( pub fn render(
&self, &mut self,
buffer: &TextBuffer, buffer: &TextBuffer,
base: &Uri, base: &Uri,
link_color: &RGBA, link_color: &RGBA,
links: &mut HashMap<TextTag, Uri>, links: &mut HashMap<TextTag, Uri>,
) -> Option<String> { ) -> Option<String> {
// * keep in order! // Collect all preformatted blocks first, and replace them with tmp macro ID
self.pre.collect(buffer);
// Keep in order!
let title = self.header.render(buffer); let title = self.header.render(buffer);
self.quote.render(buffer); self.quote.render(buffer);
@ -59,6 +66,9 @@ impl Tags {
reference::render_images(&buffer, base, &link_color, links); reference::render_images(&buffer, base, &link_color, links);
reference::render_links(&buffer, base, &link_color, links); reference::render_links(&buffer, base, &link_color, links);
self.pre.render(buffer);
// Format document title string
title.map(|mut s| { title.map(|mut s| {
s = bold::strip_tags(&s); s = bold::strip_tags(&s);
s = reference::strip_tags(&s); s = reference::strip_tags(&s);

View file

@ -0,0 +1,106 @@
use gtk::{
TextBuffer, TextSearchFlags, TextTag,
WrapMode::Word,
glib::{GString, uuid_string_random},
prelude::{TextBufferExt, TextBufferExtManual},
};
use regex::Regex;
use std::collections::HashMap;
const REGEX_PRE: &str = r"(?s)```[ \t]*(?P<alt>.*?)\n(?P<data>.*?)```";
struct Entry {
alt: Option<String>,
data: String,
}
pub struct Pre {
index: HashMap<GString, Entry>,
tag: TextTag,
}
impl Pre {
pub fn new() -> Self {
Self {
index: HashMap::new(),
tag: TextTag::builder().wrap_mode(Word).build(), // @TODO
}
}
/// Collect all preformatted blocks into `Self.index` (to prevent formatting)
pub fn collect(&mut self, buffer: &TextBuffer) {
let (start, end) = buffer.bounds();
let full_content = buffer.text(&start, &end, true).to_string();
let matches: Vec<_> = Regex::new(REGEX_PRE)
.unwrap()
.captures_iter(&full_content)
.collect();
for cap in matches.into_iter().rev() {
let id = uuid_string_random();
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, &id, &[]);
assert!(
self.index
.insert(
id,
Entry {
alt: alt(cap["alt"].into()).map(|s| s.into()),
data: cap["data"].into(),
},
)
.is_none()
)
}
}
/// Apply preformatted `Tag` to given `TextBuffer` using `Self.index`
pub fn render(&mut self, buffer: &TextBuffer) {
assert!(buffer.tag_table().add(&self.tag));
for (k, v) in self.index.iter() {
while let Some((mut m_start, mut m_end)) =
buffer
.start_iter()
.forward_search(k, TextSearchFlags::VISIBLE_ONLY, None)
{
buffer.delete(&mut m_start, &mut m_end);
let alt_text = v.alt.as_deref().unwrap_or("");
let display_text = format!("{} |\n {}", alt_text, v.data);
buffer.insert_with_tags(&mut m_start, &display_text, &[&self.tag]);
}
}
}
}
fn alt(value: Option<&str>) -> Option<&str> {
value.map(|m| m.trim()).filter(|s| !s.is_empty())
}
#[test]
fn test_regex() {
let cap: Vec<_> = Regex::new(REGEX_PRE)
.unwrap()
.captures_iter("Some ``` alt text\ncode line 1\ncode line 2``` and ```\ncode line 3\ncode line 4``` with ![img](https://link.com)")
.collect();
let first = cap.get(0).unwrap();
assert_eq!(alt(first.name("alt").map(|m| m.as_str())), Some("alt text"));
assert_eq!(&first["data"], "code line 1\ncode line 2");
let second = cap.get(1).unwrap();
assert_eq!(alt(second.name("alt").map(|m| m.as_str())), None);
assert_eq!(&second["data"], "code line 3\ncode line 4");
}