From ca9c2058edadc912de1c6b7b4508c17619b7506a Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 16 Mar 2026 09:06:31 +0200 Subject: [PATCH] implement italic tag --- .../item/page/content/text/markdown/tags.rs | 6 + .../page/content/text/markdown/tags/bold.rs | 9 +- .../page/content/text/markdown/tags/italic.rs | 141 ++++++++++++++++++ .../page/content/text/markdown/tags/quote.rs | 2 +- 4 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 src/app/browser/window/tab/item/page/content/text/markdown/tags/italic.rs 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 5085b91d..ece2ccf5 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 @@ -2,6 +2,7 @@ mod bold; mod code; mod header; mod hr; +mod italic; mod list; mod pre; mod quote; @@ -18,6 +19,7 @@ use gtk::{ prelude::{TextBufferExt, TextViewExt}, }; use header::Header; +use italic::Italic; use pre::Pre; use quote::Quote; use std::collections::HashMap; @@ -28,6 +30,7 @@ pub struct Tags { pub bold: Bold, pub code: Code, pub header: Header, + pub italic: Italic, pub pre: Pre, pub quote: Quote, pub strike: Strike, @@ -47,6 +50,7 @@ impl Tags { bold: Bold::new(), code: Code::new(), header: Header::new(), + italic: Italic::new(), pre: Pre::new(), quote: Quote::new(), strike: Strike::new(), @@ -75,6 +79,7 @@ impl Tags { self.quote.render(&buffer); self.bold.render(&buffer); + self.italic.render(&buffer); self.pre.render(&buffer); self.strike.render(&buffer); self.underline.render(&buffer); @@ -102,6 +107,7 @@ impl Tags { title.map(|mut s| { s = bold::strip_tags(&s); s = hr::strip_tags(&s); + s = italic::strip_tags(&s); s = pre::strip_tags(&s); s = reference::strip_tags(&s); s = strike::strip_tags(&s); diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/bold.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/bold.rs index 2ba656aa..013f930a 100644 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/bold.rs +++ b/src/app/browser/window/tab/item/page/content/text/markdown/tags/bold.rs @@ -14,7 +14,7 @@ impl Bold { Self(TextTag::builder().weight(600).wrap_mode(Word).build()) } - /// Apply **bold** `Tag` to given `TextBuffer` + /// Apply **bold**/__bold__ `Tag` to given `TextBuffer` pub fn render(&self, buffer: &TextBuffer) { assert!(buffer.tag_table().add(&self.0)); @@ -72,7 +72,8 @@ pub fn strip_tags(value: &str) -> String { #[test] fn test_strip_tags() { - const VALUE: &str = "Some **bold 1** and **bold 2** and __bold 3__ and *italic 1* and _italic 2_ with ![img](https://link.com)"; + const VALUE: &str = + "Some **bold 1** and **bold 2** and __bold 3__ and *italic 1* and _italic 2_"; let mut result = String::from(VALUE); for cap in Regex::new(REGEX_BOLD).unwrap().captures_iter(VALUE) { if let Some(m) = cap.get(0) { @@ -81,7 +82,7 @@ fn test_strip_tags() { } assert_eq!( result, - "Some bold 1 and bold 2 and bold 3 and *italic 1* and _italic 2_ with ![img](https://link.com)" + "Some bold 1 and bold 2 and bold 3 and *italic 1* and _italic 2_" ) } @@ -90,7 +91,7 @@ fn test_regex() { let cap: Vec<_> = Regex::new(REGEX_BOLD) .unwrap() .captures_iter( - "Some **bold 1** and **bold 2** and __bold 3__ and *italic 1* and _italic 2_ with ![img](https://link.com)" + "Some **bold 1** and **bold 2** and __bold 3__ and *italic 1* and _italic 2_", ) .collect(); diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/italic.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/italic.rs new file mode 100644 index 00000000..9c485ad8 --- /dev/null +++ b/src/app/browser/window/tab/item/page/content/text/markdown/tags/italic.rs @@ -0,0 +1,141 @@ +use gtk::{ + TextBuffer, TextTag, + WrapMode::Word, + pango::Style, + prelude::{TextBufferExt, TextBufferExtManual}, +}; +use regex::Regex; + +const REGEX_ITALIC_1: &str = r"\*(?P[^\*]*)\*"; +const REGEX_ITALIC_2: &str = r"\b_(?P[^_]*)_\b"; + +pub struct Italic(TextTag); + +impl Italic { + pub fn new() -> Self { + Self( + TextTag::builder() + .style(Style::Italic) + .wrap_mode(Word) + .build(), + ) + } + + /// Apply *italic*/_italic_ `Tag` to given `TextBuffer` + /// * run after `Bold` tag! + pub fn render(&self, buffer: &TextBuffer) { + assert!(buffer.tag_table().add(&self.0)); + + render(self, buffer, REGEX_ITALIC_1); + render(self, buffer, REGEX_ITALIC_2); + } +} + +fn render(this: &Italic, buffer: &TextBuffer, regex: &str) { + let (start, end) = buffer.bounds(); + let full_content = buffer.text(&start, &end, true).to_string(); + + let matches: Vec<_> = Regex::new(regex) + .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; + } + + let mut tags = start_iter.tags(); + tags.push(this.0.clone()); + + buffer.delete(&mut start_iter, &mut end_iter); + buffer.insert_with_tags( + &mut start_iter, + &cap["text"], + &tags.iter().collect::>(), + ) + } +} + +/// * run after `Bold` tag! +pub fn strip_tags(value: &str) -> String { + let mut s = String::from(value); + for cap in Regex::new(REGEX_ITALIC_1).unwrap().captures_iter(value) { + if let Some(m) = cap.get(0) { + s = s.replace(m.as_str(), &cap["text"]); + } + } + for cap in Regex::new(REGEX_ITALIC_2).unwrap().captures_iter(value) { + if let Some(m) = cap.get(0) { + s = s.replace(m.as_str(), &cap["text"]); + } + } + s +} + +#[test] +fn test_strip_tags() { + const S: &str = "Some *italic 1*\nand *italic 2* and _italic 3_"; + { + let mut result = String::from(S); + for cap in Regex::new(REGEX_ITALIC_1).unwrap().captures_iter(S) { + if let Some(m) = cap.get(0) { + result = result.replace(m.as_str(), &cap["text"]); + } + } + assert_eq!(result, "Some italic 1\nand italic 2 and _italic 3_") + } + { + let mut result = String::from(S); + for cap in Regex::new(REGEX_ITALIC_2).unwrap().captures_iter(S) { + if let Some(m) = cap.get(0) { + result = result.replace(m.as_str(), &cap["text"]); + } + } + assert_eq!(result, "Some *italic 1*\nand *italic 2* and italic 3") + } +} + +#[test] +fn test_regex() { + const S: &str = "Some *italic 1*\nand *italic 2* and _italic 3_"; + { + let cap: Vec<_> = Regex::new(REGEX_ITALIC_1) + .unwrap() + .captures_iter(S) + .collect(); + + assert_eq!(cap.len(), 2); + + let mut c = cap.into_iter(); + assert_eq!(&c.next().unwrap()["text"], "italic 1"); + assert_eq!(&c.next().unwrap()["text"], "italic 2"); + } + { + let cap: Vec<_> = Regex::new(REGEX_ITALIC_2) + .unwrap() + .captures_iter(S) + .collect(); + + assert_eq!(cap.len(), 1); + + let mut c = cap.into_iter(); + assert_eq!(&c.next().unwrap()["text"], "italic 3"); + } +} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/quote.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/quote.rs index cd011fd7..4c43fbfc 100644 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/quote.rs +++ b/src/app/browser/window/tab/item/page/content/text/markdown/tags/quote.rs @@ -16,7 +16,7 @@ impl Quote { TextTag::builder() .left_margin(28) .wrap_mode(Word) - .style(Italic) // what about the italic tags decoration? @TODO + .style(Italic) // conflicts the italic tags decoration @TODO .build(), ) }