implement italic tag

This commit is contained in:
yggverse 2026-03-16 09:06:31 +02:00
parent 2ef5e52079
commit ca9c2058ed
4 changed files with 153 additions and 5 deletions

View file

@ -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);

View file

@ -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();

View file

@ -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<text>[^\*]*)\*";
const REGEX_ITALIC_2: &str = r"\b_(?P<text>[^_]*)_\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::<Vec<&TextTag>>(),
)
}
}
/// * 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");
}
}

View file

@ -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(),
)
}