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 508a93d6..34be39c0 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 @@ -2,9 +2,8 @@ mod ansi; pub mod error; mod gutter; mod icon; -mod reference; mod syntax; -mod tag; +mod tags; use super::{ItemAction, WindowAction}; use crate::app::browser::window::action::Position; @@ -22,7 +21,7 @@ use icon::Icon; use sourceview::prelude::{ActionExt, ActionMapExt, DisplayExt, ToVariant}; use std::{cell::Cell, collections::HashMap, rc::Rc}; use syntax::Syntax; -use tag::Tag; +use tags::Tags; pub const NEW_LINE: &str = "\n"; @@ -70,10 +69,10 @@ impl Markdown { let icon = Icon::new(); // Init tags - let tag = Tag::new(); + let tags = Tags::new(); // Init new text buffer - let buffer = TextBuffer::new(Some(&tag.text_tag_table)); + let buffer = TextBuffer::new(Some(&tags.text_tag_table)); buffer.set_text(markdown); // Init main widget @@ -111,12 +110,7 @@ impl Markdown { // Render markdown tags // * keep in order! - tag::header(&buffer, &tag); - tag::quote(&buffer, &tag); - - reference::image_link(&buffer, &tag, base, &link_color.0, &mut links); - reference::image(&buffer, &tag, base, &link_color.0, &mut links); - reference::link(&buffer, &tag, base, &link_color.0, &mut links); + tags.render(&buffer, &base, &link_color.0, &mut links); // Parse single-line markdown tags /*'l: for line in markdown.lines() { diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tag.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tag.rs deleted file mode 100644 index cc298c95..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tag.rs +++ /dev/null @@ -1,173 +0,0 @@ -mod header; -mod list; -mod plain; -mod quote; -mod title; - -use gtk::{ - TextBuffer, TextTag, TextTagTable, - prelude::{TextBufferExt, TextBufferExtManual}, -}; -use header::Header; -use list::List; -use plain::Plain; -use quote::Quote; -use regex::Regex; -use title::Title; - -pub struct Tag { - pub text_tag_table: TextTagTable, - // Tags - pub h1: TextTag, - pub h2: TextTag, - pub h3: TextTag, - pub h4: TextTag, - pub h5: TextTag, - pub h6: TextTag, - pub list: TextTag, - pub quote: TextTag, - pub title: TextTag, - pub plain: TextTag, -} - -impl Default for Tag { - fn default() -> Self { - Self::new() - } -} - -impl Tag { - // Construct - pub fn new() -> Self { - // Init components - let h1 = TextTag::h1(); - let h2 = TextTag::h2(); - let h3 = TextTag::h3(); - let h4 = TextTag::h4(); - let h5 = TextTag::h5(); - let h6 = TextTag::h6(); - let list = TextTag::list(); - let quote = TextTag::quote(); - let title = TextTag::title(); - let plain = TextTag::plain(); - - // Init tag table - let text_tag_table = TextTagTable::new(); - - text_tag_table.add(&h1); - text_tag_table.add(&h2); - text_tag_table.add(&h3); - text_tag_table.add(&h4); - text_tag_table.add(&h5); - text_tag_table.add(&h6); - text_tag_table.add(&title); - text_tag_table.add(&list); - text_tag_table.add("e); - text_tag_table.add(&plain); - - Self { - text_tag_table, - // Tags - h1, - h2, - h3, - h4, - h5, - h6, - list, - quote, - title, - plain, - } - } -} - -// Headers `#`, `##`, etc. - -const REGEX_HEADER: &str = r"(?m)^(?P#{1,6})\s+(?P.*)$"; - -/// Apply header `Tag` to given `TextBuffer` -pub fn header(buffer: &TextBuffer, tag: &Tag) { - let (start, end) = buffer.bounds(); - let full_content = buffer.text(&start, &end, true).to_string(); - - let matches: Vec<_> = Regex::new(REGEX_HEADER) - .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); - - match cap["level"].chars().count() { - 1 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&tag.h1]), - 2 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&tag.h2]), - 3 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&tag.h3]), - 4 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&tag.h4]), - 5 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&tag.h5]), - 6 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&tag.h6]), - _ => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[]), - } - } -} - -#[test] -fn test_regex_header() { - let cap: Vec<_> = Regex::new(REGEX_HEADER) - .unwrap() - .captures_iter(r"## Title ![alt](https://link.com)") - .collect(); - - let first = cap.get(0).unwrap(); - assert_eq!(&first[0], "## Title ![alt](https://link.com)"); - assert_eq!(&first["level"], "##"); - assert_eq!(&first["title"], "Title ![alt](https://link.com)"); -} - -// Quotes - -const REGEX_QUOTE: &str = r"(?m)^>\s+(?P<text>.*)$"; - -/// Apply quote `Tag` to given `TextBuffer` -pub fn quote(buffer: &TextBuffer, tag: &Tag) { - let (start, end) = buffer.bounds(); - let full_content = buffer.text(&start, &end, true).to_string(); - - let matches: Vec<_> = Regex::new(REGEX_QUOTE) - .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"], &[&tag.quote]) - } -} - -#[test] -fn test_regex_quote() { - let cap: Vec<_> = Regex::new(REGEX_QUOTE) - .unwrap() - .captures_iter(r"> Some quote with ![img](https://link.com)") - .collect(); - - let first = cap.get(0).unwrap(); - assert_eq!(&first[0], "> Some quote with ![img](https://link.com)"); - assert_eq!(&first["text"], "Some quote with ![img](https://link.com)"); -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tag/header.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tag/header.rs deleted file mode 100644 index 2a376692..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tag/header.rs +++ /dev/null @@ -1,67 +0,0 @@ -use gtk::{TextTag, WrapMode}; - -pub trait Header { - fn h1() -> Self; - fn h2() -> Self; - fn h3() -> Self; - fn h4() -> Self; - fn h5() -> Self; - fn h6() -> Self; -} - -impl Header for TextTag { - fn h1() -> Self { - TextTag::builder() - .foreground("#2190a4") // @TODO optional - .scale(1.6) - .sentence(true) - .weight(500) - .wrap_mode(WrapMode::Word) - .build() - } - fn h2() -> Self { - TextTag::builder() - .foreground("#d56199") // @TODO optional - .scale(1.4) - .sentence(true) - .weight(400) - .wrap_mode(WrapMode::Word) - .build() - } - fn h3() -> Self { - TextTag::builder() - .foreground("#c88800") // @TODO optional - .scale(1.2) - .sentence(true) - .weight(400) - .wrap_mode(WrapMode::Word) - .build() - } - fn h4() -> Self { - TextTag::builder() - .foreground("#c88800") // @TODO optional - .scale(1.1) - .sentence(true) - .weight(400) - .wrap_mode(WrapMode::Word) - .build() - } - fn h5() -> Self { - TextTag::builder() - .foreground("#c88800") // @TODO optional - .scale(1.0) - .sentence(true) - .weight(400) - .wrap_mode(WrapMode::Word) - .build() - } - fn h6() -> Self { - TextTag::builder() - .foreground("#c88800") // @TODO optional - .scale(1.0) - .sentence(true) - .weight(300) - .wrap_mode(WrapMode::Word) - .build() - } -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tag/quote.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tag/quote.rs deleted file mode 100644 index 8b937a76..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tag/quote.rs +++ /dev/null @@ -1,15 +0,0 @@ -use gtk::{TextTag, WrapMode::Word, pango::Style::Italic}; - -pub trait Quote { - fn quote() -> Self; -} - -impl Quote for TextTag { - fn quote() -> Self { - TextTag::builder() - .left_margin(28) - .wrap_mode(Word) - .style(Italic) // what about the italic tags decoration? @TODO - .build() - } -} 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 new file mode 100644 index 00000000..494db800 --- /dev/null +++ b/src/app/browser/window/tab/item/page/content/text/markdown/tags.rs @@ -0,0 +1,78 @@ +mod header; +mod list; +mod plain; +mod quote; +mod reference; +mod title; + +use std::collections::HashMap; + +use gtk::{ + TextBuffer, TextTag, TextTagTable, + gdk::RGBA, + glib::Uri, + prelude::{TextBufferExt, TextBufferExtManual}, +}; +use header::Header; +use list::List; +use plain::Plain; +use quote::Quote; +use reference::Reference; +use title::Title; + +pub struct Tags { + pub text_tag_table: TextTagTable, + // Tags + pub header: Header, + pub list: TextTag, + pub plain: TextTag, + pub quote: Quote, + pub title: TextTag, +} + +impl Default for Tags { + fn default() -> Self { + Self::new() + } +} + +impl Tags { + // Construct + pub fn new() -> Self { + // Init tag table + let text_tag_table = TextTagTable::new(); + + // Init components + let list = TextTag::list(); + let plain = TextTag::plain(); + let title = TextTag::title(); + text_tag_table.add(&title); + text_tag_table.add(&list); + text_tag_table.add(&plain); + + Self { + text_tag_table, + // Tags + header: Header::new(), + list, + plain, + quote: Quote::new(), + title, + } + } + pub fn render( + &self, + buffer: &TextBuffer, + base: &Uri, + link_color: &RGBA, + links: &mut HashMap<TextTag, Uri>, + ) { + // * keep in order! + self.header.render(buffer); + self.quote.render(buffer); + + reference::render_images_links(&buffer, base, &link_color, links); + reference::render_images(&buffer, base, &link_color, links); + reference::render_links(&buffer, base, &link_color, links); + } +} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/header.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/header.rs new file mode 100644 index 00000000..44ae5f68 --- /dev/null +++ b/src/app/browser/window/tab/item/page/content/text/markdown/tags/header.rs @@ -0,0 +1,119 @@ +use gtk::{ + TextBuffer, TextTag, WrapMode, + prelude::{TextBufferExt, TextBufferExtManual}, +}; +use regex::Regex; + +const REGEX_HEADER: &str = r"(?m)^(?P<level>#{1,6})\s+(?P<title>.*)$"; + +pub struct Header { + h1: TextTag, + h2: TextTag, + h3: TextTag, + h4: TextTag, + h5: TextTag, + h6: TextTag, +} + +impl Header { + pub fn new() -> Self { + Self { + h1: TextTag::builder() + .foreground("#2190a4") // @TODO optional + .scale(1.6) + .sentence(true) + .weight(500) + .wrap_mode(WrapMode::Word) + .build(), + h2: TextTag::builder() + .foreground("#d56199") // @TODO optional + .scale(1.4) + .sentence(true) + .weight(400) + .wrap_mode(WrapMode::Word) + .build(), + h3: TextTag::builder() + .foreground("#c88800") // @TODO optional + .scale(1.2) + .sentence(true) + .weight(400) + .wrap_mode(WrapMode::Word) + .build(), + h4: TextTag::builder() + .foreground("#c88800") // @TODO optional + .scale(1.1) + .sentence(true) + .weight(400) + .wrap_mode(WrapMode::Word) + .build(), + h5: TextTag::builder() + .foreground("#c88800") // @TODO optional + .scale(1.0) + .sentence(true) + .weight(400) + .wrap_mode(WrapMode::Word) + .build(), + h6: TextTag::builder() + .foreground("#c88800") // @TODO optional + .scale(1.0) + .sentence(true) + .weight(300) + .wrap_mode(WrapMode::Word) + .build(), + } + } + + /// Apply title `Tag` to given `TextBuffer` + pub fn render(&self, buffer: &TextBuffer) { + let table = buffer.tag_table(); + + assert!(table.add(&self.h1)); + assert!(table.add(&self.h2)); + assert!(table.add(&self.h3)); + assert!(table.add(&self.h4)); + assert!(table.add(&self.h5)); + + let (start, end) = buffer.bounds(); + let full_content = buffer.text(&start, &end, true).to_string(); + + let matches: Vec<_> = Regex::new(REGEX_HEADER) + .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); + + match cap["level"].chars().count() { + 1 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h1]), + 2 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h2]), + 3 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h3]), + 4 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h4]), + 5 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h5]), + 6 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&self.h6]), + _ => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[]), + } + } + } +} + +#[test] +fn test_regex_title() { + let cap: Vec<_> = Regex::new(REGEX_HEADER) + .unwrap() + .captures_iter(r"## Header ![alt](https://link.com)") + .collect(); + + let first = cap.get(0).unwrap(); + assert_eq!(&first[0], "## Header ![alt](https://link.com)"); + assert_eq!(&first["level"], "##"); + assert_eq!(&first["title"], "Header ![alt](https://link.com)"); +} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tag/list.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/list.rs similarity index 100% rename from src/app/browser/window/tab/item/page/content/text/markdown/tag/list.rs rename to src/app/browser/window/tab/item/page/content/text/markdown/tags/list.rs diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tag/plain.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/plain.rs similarity index 100% rename from src/app/browser/window/tab/item/page/content/text/markdown/tag/plain.rs rename to src/app/browser/window/tab/item/page/content/text/markdown/tags/plain.rs 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 new file mode 100644 index 00000000..17db7bb5 --- /dev/null +++ b/src/app/browser/window/tab/item/page/content/text/markdown/tags/quote.rs @@ -0,0 +1,61 @@ +use gtk::{ + TextBuffer, TextTag, + WrapMode::Word, + pango::Style::Italic, + prelude::{TextBufferExt, TextBufferExtManual}, +}; +use regex::Regex; + +const REGEX_QUOTE: &str = r"(?m)^>\s+(?P<text>.*)$"; + +pub struct Quote(TextTag); + +impl Quote { + pub fn new() -> Self { + Self( + TextTag::builder() + .left_margin(28) + .wrap_mode(Word) + .style(Italic) // what about the italic tags decoration? @TODO + .build(), + ) + } + + /// Apply quote `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_QUOTE) + .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]) + } + } +} + +#[test] +fn test_regex() { + let cap: Vec<_> = Regex::new(REGEX_QUOTE) + .unwrap() + .captures_iter(r"> Some quote with ![img](https://link.com)") + .collect(); + + let first = cap.get(0).unwrap(); + assert_eq!(&first[0], "> Some quote with ![img](https://link.com)"); + assert_eq!(&first["text"], "Some quote with ![img](https://link.com)"); +} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/reference.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/reference.rs similarity index 90% rename from src/app/browser/window/tab/item/page/content/text/markdown/reference.rs rename to src/app/browser/window/tab/item/page/content/text/markdown/tags/reference.rs index 4400eb21..ef0f80d6 100644 --- a/src/app/browser/window/tab/item/page/content/text/markdown/reference.rs +++ b/src/app/browser/window/tab/item/page/content/text/markdown/tags/reference.rs @@ -1,4 +1,3 @@ -use super::Tag; use gtk::{ TextBuffer, TextIter, TextTag, WrapMode, gdk::RGBA, @@ -14,11 +13,12 @@ const REGEX_IMAGE_LINK: &str = r"\[(?P<is_img>!)\[(?P<alt>[^\]]+)\]\((?P<img_url>[^\)]+)\)\]\((?P<link_url>[^\)]+)\)"; pub struct Reference { - pub uri: Uri, - pub alt: String, + uri: Uri, + alt: String, } impl Reference { + /// Try construct new `Self` with given options fn parse(address: &str, alt: Option<&str>, base: &Uri) -> Option<Self> { // Convert address to the valid URI, // resolve to absolute URL format if the target is relative @@ -63,12 +63,13 @@ impl Reference { Err(_) => None, } } + + /// Insert `Self` into the given `TextBuffer` by registering new `TextTag` created fn into_buffer( self, buffer: &TextBuffer, position: &mut TextIter, link_color: &RGBA, - tag: &Tag, is_annotation: bool, links: &mut HashMap<TextTag, Uri>, ) { @@ -93,18 +94,15 @@ impl Reference { .wrap_mode(WrapMode::Word) .build() }; - if !tag.text_tag_table.add(&a) { - panic!() - } + assert!(buffer.tag_table().add(&a)); buffer.insert_with_tags(position, &self.alt, &[&a]); links.insert(a, self.uri); } } /// Image links `[![]()]()` -pub fn image_link( +pub fn render_images_links( buffer: &TextBuffer, - tag: &Tag, base: &Uri, link_color: &RGBA, links: &mut HashMap<TextTag, Uri>, @@ -128,7 +126,7 @@ pub fn image_link( buffer.delete(&mut start_iter, &mut end_iter); - if let Some(reference) = Reference::parse( + if let Some(this) = Reference::parse( &cap["img_url"], if cap["alt"].is_empty() { None @@ -137,17 +135,16 @@ pub fn image_link( }, base, ) { - reference.into_buffer(buffer, &mut start_iter, link_color, tag, false, links) + this.into_buffer(buffer, &mut start_iter, link_color, false, links) } - if let Some(reference) = Reference::parse(&cap["link_url"], Some("1"), base) { - reference.into_buffer(buffer, &mut start_iter, link_color, tag, true, links) + if let Some(this) = Reference::parse(&cap["link_url"], Some("1"), base) { + this.into_buffer(buffer, &mut start_iter, link_color, true, links) } } } /// Image tags `![]()` -pub fn image( +pub fn render_images( buffer: &TextBuffer, - tag: &Tag, base: &Uri, link_color: &RGBA, links: &mut HashMap<TextTag, Uri>, @@ -171,7 +168,7 @@ pub fn image( buffer.delete(&mut start_iter, &mut end_iter); - if let Some(reference) = Reference::parse( + if let Some(this) = Reference::parse( &cap["url"], if cap["alt"].is_empty() { None @@ -180,14 +177,13 @@ pub fn image( }, base, ) { - reference.into_buffer(buffer, &mut start_iter, link_color, tag, false, links) + this.into_buffer(buffer, &mut start_iter, link_color, false, links) } } } /// Links `[]()` -pub fn link( +pub fn render_links( buffer: &TextBuffer, - tag: &Tag, base: &Uri, link_color: &RGBA, links: &mut HashMap<TextTag, Uri>, @@ -211,7 +207,7 @@ pub fn link( buffer.delete(&mut start_iter, &mut end_iter); - if let Some(reference) = Reference::parse( + if let Some(this) = Reference::parse( &cap["url"], if cap["text"].is_empty() { None @@ -220,7 +216,7 @@ pub fn link( }, base, ) { - reference.into_buffer(buffer, &mut start_iter, link_color, tag, false, links) + this.into_buffer(buffer, &mut start_iter, link_color, false, links) } } } diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tag/title.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/title.rs similarity index 100% rename from src/app/browser/window/tab/item/page/content/text/markdown/tag/title.rs rename to src/app/browser/window/tab/item/page/content/text/markdown/tags/title.rs