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 fb15924b..78a7df54 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 @@ -6,7 +6,6 @@ mod tags; use super::{ItemAction, WindowAction}; use crate::app::browser::window::action::Position; -pub use error::Error; use gtk::{ EventControllerMotion, GestureClick, TextBuffer, TextTag, TextTagTable, TextView, TextWindowType, UriLauncher, Window, WrapMode, @@ -56,7 +55,7 @@ impl Markdown { let syntax = Syntax::new(); // Init tags - let tags = Tags::new(); + let mut tags = Tags::new(); // Init new text buffer let buffer = TextBuffer::new(Some(&TextTagTable::new())); 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 ec3d0aa4..779c236d 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,5 +1,6 @@ mod bold; mod header; +mod pre; mod quote; mod reference; mod strike; @@ -10,6 +11,7 @@ use std::collections::HashMap; use bold::Bold; use gtk::{TextBuffer, TextTag, gdk::RGBA, glib::Uri}; use header::Header; +use pre::Pre; use quote::Quote; use strike::Strike; use underline::Underline; @@ -17,6 +19,7 @@ use underline::Underline; pub struct Tags { pub bold: Bold, pub header: Header, + pub pre: Pre, pub quote: Quote, pub strike: Strike, pub underline: Underline, @@ -34,19 +37,23 @@ impl Tags { Self { bold: Bold::new(), header: Header::new(), + pre: Pre::new(), quote: Quote::new(), strike: Strike::new(), underline: Underline::new(), } } pub fn render( - &self, + &mut self, buffer: &TextBuffer, base: &Uri, link_color: &RGBA, links: &mut HashMap, ) -> Option { - // * 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); self.quote.render(buffer); @@ -59,6 +66,9 @@ impl Tags { reference::render_images(&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| { s = bold::strip_tags(&s); s = reference::strip_tags(&s); diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/pre.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/pre.rs new file mode 100644 index 00000000..dd0df5c0 --- /dev/null +++ b/src/app/browser/window/tab/item/page/content/text/markdown/tags/pre.rs @@ -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.*?)\n(?P.*?)```"; + +struct Entry { + alt: Option, + data: String, +} + +pub struct Pre { + index: HashMap, + 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"); +}