mirror of
https://github.com/YGGverse/Yoda.git
synced 2026-03-31 16:45:27 +00:00
implement link, linked images, and images parser; temporarily disable header impl
This commit is contained in:
parent
5675809320
commit
9843d49326
2 changed files with 183 additions and 108 deletions
|
|
@ -19,8 +19,6 @@ use gtk::{
|
||||||
};
|
};
|
||||||
use gutter::Gutter;
|
use gutter::Gutter;
|
||||||
use icon::Icon;
|
use icon::Icon;
|
||||||
use reference::Reference;
|
|
||||||
use regex::Regex;
|
|
||||||
use sourceview::prelude::{ActionExt, ActionMapExt, DisplayExt, ToVariant};
|
use sourceview::prelude::{ActionExt, ActionMapExt, DisplayExt, ToVariant};
|
||||||
use std::{cell::Cell, collections::HashMap, rc::Rc};
|
use std::{cell::Cell, collections::HashMap, rc::Rc};
|
||||||
use syntax::Syntax;
|
use syntax::Syntax;
|
||||||
|
|
@ -53,7 +51,7 @@ impl Markdown {
|
||||||
let hover: Rc<Cell<Option<TextTag>>> = Rc::new(Cell::new(None));
|
let hover: Rc<Cell<Option<TextTag>>> = Rc::new(Cell::new(None));
|
||||||
|
|
||||||
// Init code features
|
// Init code features
|
||||||
let mut code = None;
|
//let mut code = None;
|
||||||
|
|
||||||
// Init quote icon feature
|
// Init quote icon feature
|
||||||
let mut is_line_after_quote = false;
|
let mut is_line_after_quote = false;
|
||||||
|
|
@ -76,6 +74,7 @@ impl Markdown {
|
||||||
|
|
||||||
// Init new text buffer
|
// Init new text buffer
|
||||||
let buffer = TextBuffer::new(Some(&tag.text_tag_table));
|
let buffer = TextBuffer::new(Some(&tag.text_tag_table));
|
||||||
|
buffer.set_text(markdown);
|
||||||
|
|
||||||
// Init main widget
|
// Init main widget
|
||||||
let text_view = {
|
let text_view = {
|
||||||
|
|
@ -109,8 +108,15 @@ impl Markdown {
|
||||||
t == 0 || t.is_multiple_of(2)
|
t == 0 || t.is_multiple_of(2)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Parse in-line markdown tags
|
||||||
|
// * keep order!
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
// Parse single-line markdown tags
|
// Parse single-line markdown tags
|
||||||
'l: for line in markdown.lines() {
|
/*'l: for line in markdown.lines() {
|
||||||
if is_code_enabled {
|
if is_code_enabled {
|
||||||
use ggemtext::line::Code;
|
use ggemtext::line::Code;
|
||||||
match code {
|
match code {
|
||||||
|
|
@ -254,12 +260,7 @@ impl Markdown {
|
||||||
// just append plain text covered in empty tag (to handle controller events properly)
|
// just append plain text covered in empty tag (to handle controller events properly)
|
||||||
buffer.insert_with_tags(&mut buffer.end_iter(), line, &[&tag.plain]);
|
buffer.insert_with_tags(&mut buffer.end_iter(), line, &[&tag.plain]);
|
||||||
buffer.insert(&mut buffer.end_iter(), NEW_LINE);
|
buffer.insert(&mut buffer.end_iter(), NEW_LINE);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
// Parse in-line markdown tags
|
|
||||||
|
|
||||||
image_link(&buffer, &tag, base, &link_color.0, &mut links);
|
|
||||||
link(&buffer, &tag, base, &link_color.0, &mut links);
|
|
||||||
|
|
||||||
// Context menu
|
// Context menu
|
||||||
let action_link_tab =
|
let action_link_tab =
|
||||||
|
|
@ -557,91 +558,6 @@ fn link_prefix(request: String, prefix: &str) -> String {
|
||||||
format!("{prefix}{}", request.trim_start_matches(prefix))
|
format!("{prefix}{}", request.trim_start_matches(prefix))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Link
|
|
||||||
fn image_link(
|
|
||||||
buffer: &TextBuffer,
|
|
||||||
tag: &Tag,
|
|
||||||
base: &Uri,
|
|
||||||
link_color: &RGBA,
|
|
||||||
links: &mut HashMap<TextTag, Uri>,
|
|
||||||
) {
|
|
||||||
let start_iter = buffer.start_iter();
|
|
||||||
let end_iter = buffer.end_iter();
|
|
||||||
let full_content = buffer.text(&start_iter, &end_iter, true).to_string();
|
|
||||||
|
|
||||||
buffer.set_text("");
|
|
||||||
|
|
||||||
let mut last_pos = 0;
|
|
||||||
for cap in Regex::new(r"(?P<full_match>\[(?P<is_img>!|)?\[(?P<alt>[^\]]+)\]\((?P<img_url>[^\)]+)\)\]\((?P<link_url>[^\)]+)\))")
|
|
||||||
.unwrap()
|
|
||||||
.captures_iter(&full_content)
|
|
||||||
{
|
|
||||||
let full_match = cap.get(0).unwrap();
|
|
||||||
let before = &full_content[last_pos..full_match.start()];
|
|
||||||
if !before.is_empty() {
|
|
||||||
buffer.insert(&mut buffer.end_iter(), before);
|
|
||||||
}
|
|
||||||
if let Some(link) = Reference::parse(
|
|
||||||
&cap["link_url"],
|
|
||||||
None,
|
|
||||||
base,
|
|
||||||
) {
|
|
||||||
link.into_buffer(buffer,
|
|
||||||
link_color,
|
|
||||||
tag,
|
|
||||||
links)
|
|
||||||
}
|
|
||||||
last_pos = full_match.end();
|
|
||||||
}
|
|
||||||
let after = &full_content[last_pos..];
|
|
||||||
if !after.is_empty() {
|
|
||||||
buffer.insert(&mut buffer.end_iter(), after);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Link
|
|
||||||
fn link(
|
|
||||||
buffer: &TextBuffer,
|
|
||||||
tag: &Tag,
|
|
||||||
base: &Uri,
|
|
||||||
link_color: &RGBA,
|
|
||||||
links: &mut HashMap<TextTag, Uri>,
|
|
||||||
) {
|
|
||||||
let start_iter = buffer.start_iter();
|
|
||||||
let end_iter = buffer.end_iter();
|
|
||||||
let full_content = buffer.text(&start_iter, &end_iter, true).to_string();
|
|
||||||
|
|
||||||
buffer.set_text("");
|
|
||||||
|
|
||||||
let mut last_pos = 0;
|
|
||||||
for cap in Regex::new(r"(?P<is_img>!)\[(?P<text>[^\]]+)\]\((?P<url>[^\)]+)\)")
|
|
||||||
.unwrap()
|
|
||||||
.captures_iter(&full_content)
|
|
||||||
{
|
|
||||||
let full_match = cap.get(0).unwrap();
|
|
||||||
let before = &full_content[last_pos..full_match.start()];
|
|
||||||
if !before.is_empty() {
|
|
||||||
buffer.insert(&mut buffer.end_iter(), before);
|
|
||||||
}
|
|
||||||
if let Some(link) = Reference::parse(
|
|
||||||
&cap["url"],
|
|
||||||
if cap["text"].is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(&cap["text"])
|
|
||||||
},
|
|
||||||
base,
|
|
||||||
) {
|
|
||||||
link.into_buffer(buffer, link_color, tag, links)
|
|
||||||
}
|
|
||||||
last_pos = full_match.end();
|
|
||||||
}
|
|
||||||
let after = &full_content[last_pos..];
|
|
||||||
if !after.is_empty() {
|
|
||||||
buffer.insert(&mut buffer.end_iter(), after);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Header tag
|
/// Header tag
|
||||||
fn header(buffer: &TextBuffer, tag: &TextTag, line: &str, pattern: &str) -> Option<String> {
|
fn header(buffer: &TextBuffer, tag: &TextTag, line: &str, pattern: &str) -> Option<String> {
|
||||||
if let Some(h) = line.trim_start().strip_prefix(pattern)
|
if let Some(h) = line.trim_start().strip_prefix(pattern)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,16 @@
|
||||||
use gtk::glib::{Uri, UriFlags};
|
use super::Tag;
|
||||||
|
use gtk::{
|
||||||
|
TextBuffer, TextIter, TextTag, WrapMode,
|
||||||
|
gdk::RGBA,
|
||||||
|
glib::{Uri, UriFlags},
|
||||||
|
prelude::{TextBufferExt, TextBufferExtManual},
|
||||||
|
};
|
||||||
|
use regex::Regex;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub const REGEX_LINK: &str = r"\[(?P<text>[^\]]+)\]\((?P<url>[^\)]+)\)";
|
const REGEX_LINK: &str = r"\[(?P<text>[^\]]+)\]\((?P<url>[^\)]+)\)";
|
||||||
|
const REGEX_IMAGE: &str = r"!\[(?P<alt>[^\]]+)\]\((?P<url>[^\)]+)\)";
|
||||||
pub const REGEX_IMAGE_LINK: &str =
|
const REGEX_IMAGE_LINK: &str =
|
||||||
r"\[(?P<is_img>!)\[(?P<alt>[^\]]+)\]\((?P<img_url>[^\)]+)\)\]\((?P<link_url>[^\)]+)\)";
|
r"\[(?P<is_img>!)\[(?P<alt>[^\]]+)\]\((?P<img_url>[^\)]+)\)\]\((?P<link_url>[^\)]+)\)";
|
||||||
|
|
||||||
pub struct Reference {
|
pub struct Reference {
|
||||||
|
|
@ -11,7 +19,7 @@ pub struct Reference {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Reference {
|
impl Reference {
|
||||||
pub fn parse(address: &str, alt: Option<&str>, base: &Uri) -> Option<Self> {
|
fn parse(address: &str, alt: Option<&str>, base: &Uri) -> Option<Self> {
|
||||||
// Convert address to the valid URI,
|
// Convert address to the valid URI,
|
||||||
// resolve to absolute URL format if the target is relative
|
// resolve to absolute URL format if the target is relative
|
||||||
match Uri::resolve_relative(
|
match Uri::resolve_relative(
|
||||||
|
|
@ -55,16 +63,15 @@ impl Reference {
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn into_buffer(
|
fn into_buffer(
|
||||||
self,
|
self,
|
||||||
buffer: >k::TextBuffer,
|
buffer: &TextBuffer,
|
||||||
position: &mut gtk::TextIter,
|
position: &mut TextIter,
|
||||||
link_color: >k::gdk::RGBA,
|
link_color: &RGBA,
|
||||||
tag: &super::Tag,
|
tag: &super::Tag,
|
||||||
is_annotation: bool,
|
is_annotation: bool,
|
||||||
links: &mut std::collections::HashMap<gtk::TextTag, Uri>,
|
links: &mut HashMap<TextTag, Uri>,
|
||||||
) {
|
) {
|
||||||
use gtk::{TextTag, WrapMode, prelude::TextBufferExtManual};
|
|
||||||
let a = if is_annotation {
|
let a = if is_annotation {
|
||||||
buffer.insert_with_tags(position, " ", &[]);
|
buffer.insert_with_tags(position, " ", &[]);
|
||||||
TextTag::builder()
|
TextTag::builder()
|
||||||
|
|
@ -94,25 +101,151 @@ impl Reference {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Image links `[![]()]()`
|
||||||
|
pub fn image_link(
|
||||||
|
buffer: &TextBuffer,
|
||||||
|
tag: &Tag,
|
||||||
|
base: &Uri,
|
||||||
|
link_color: &RGBA,
|
||||||
|
links: &mut HashMap<TextTag, Uri>,
|
||||||
|
) {
|
||||||
|
let (start, end) = buffer.bounds();
|
||||||
|
let full_content = buffer.text(&start, &end, true).to_string();
|
||||||
|
|
||||||
|
let matches: Vec<_> = Regex::new(REGEX_IMAGE_LINK)
|
||||||
|
.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);
|
||||||
|
|
||||||
|
if let Some(reference) = Reference::parse(
|
||||||
|
&cap["img_url"],
|
||||||
|
if cap["alt"].is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(&cap["alt"])
|
||||||
|
},
|
||||||
|
base,
|
||||||
|
) {
|
||||||
|
reference.into_buffer(buffer, &mut start_iter, link_color, tag, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Image tags `![]()`
|
||||||
|
pub fn image(
|
||||||
|
buffer: &TextBuffer,
|
||||||
|
tag: &Tag,
|
||||||
|
base: &Uri,
|
||||||
|
link_color: &RGBA,
|
||||||
|
links: &mut HashMap<TextTag, Uri>,
|
||||||
|
) {
|
||||||
|
let (start, end) = buffer.bounds();
|
||||||
|
let full_content = buffer.text(&start, &end, true).to_string();
|
||||||
|
|
||||||
|
let matches: Vec<_> = Regex::new(REGEX_IMAGE)
|
||||||
|
.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);
|
||||||
|
|
||||||
|
if let Some(reference) = Reference::parse(
|
||||||
|
&cap["url"],
|
||||||
|
if cap["alt"].is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(&cap["alt"])
|
||||||
|
},
|
||||||
|
base,
|
||||||
|
) {
|
||||||
|
reference.into_buffer(buffer, &mut start_iter, link_color, tag, false, links)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Links `[]()`
|
||||||
|
pub fn link(
|
||||||
|
buffer: &TextBuffer,
|
||||||
|
tag: &Tag,
|
||||||
|
base: &Uri,
|
||||||
|
link_color: &RGBA,
|
||||||
|
links: &mut HashMap<TextTag, Uri>,
|
||||||
|
) {
|
||||||
|
let (start, end) = buffer.bounds();
|
||||||
|
let full_content = buffer.text(&start, &end, true).to_string();
|
||||||
|
|
||||||
|
let matches: Vec<_> = Regex::new(REGEX_LINK)
|
||||||
|
.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);
|
||||||
|
|
||||||
|
if let Some(reference) = Reference::parse(
|
||||||
|
&cap["url"],
|
||||||
|
if cap["text"].is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(&cap["text"])
|
||||||
|
},
|
||||||
|
base,
|
||||||
|
) {
|
||||||
|
reference.into_buffer(buffer, &mut start_iter, link_color, tag, false, links)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_regex_link() {
|
fn test_regex_link() {
|
||||||
let cap: Vec<_> = regex::Regex::new(REGEX_LINK)
|
let cap: Vec<_> = Regex::new(REGEX_LINK)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.captures_iter(r#"[link1](https://link1.com) [link2](https://link2.com)"#)
|
.captures_iter(r#"[link1](https://link1.com) [link2](https://link2.com)"#)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let first = cap.get(0).unwrap();
|
let first = cap.get(0).unwrap();
|
||||||
|
assert_eq!(&first[0], "[link1](https://link1.com)");
|
||||||
assert_eq!(&first["text"], "link1");
|
assert_eq!(&first["text"], "link1");
|
||||||
assert_eq!(&first["url"], "https://link1.com");
|
assert_eq!(&first["url"], "https://link1.com");
|
||||||
|
|
||||||
let second = cap.get(1).unwrap();
|
let second = cap.get(1).unwrap();
|
||||||
|
assert_eq!(&second[0], "[link2](https://link2.com)");
|
||||||
assert_eq!(&second["text"], "link2");
|
assert_eq!(&second["text"], "link2");
|
||||||
assert_eq!(&second["url"], "https://link2.com");
|
assert_eq!(&second["url"], "https://link2.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_regex_image_link() {
|
fn test_regex_image_link() {
|
||||||
let cap: Vec<_> = regex::Regex::new(
|
let cap: Vec<_> = Regex::new(
|
||||||
REGEX_IMAGE_LINK,
|
REGEX_IMAGE_LINK,
|
||||||
)
|
)
|
||||||
.unwrap().captures_iter(
|
.unwrap().captures_iter(
|
||||||
|
|
@ -120,12 +253,38 @@ fn test_regex_image_link() {
|
||||||
).collect();
|
).collect();
|
||||||
|
|
||||||
let first = cap.get(0).unwrap();
|
let first = cap.get(0).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
&first[0],
|
||||||
|
"[](https://image2.com)"
|
||||||
|
);
|
||||||
assert_eq!(&first["alt"], "image1");
|
assert_eq!(&first["alt"], "image1");
|
||||||
assert_eq!(&first["img_url"], "https://image1.com");
|
assert_eq!(&first["img_url"], "https://image1.com");
|
||||||
assert_eq!(&first["link_url"], "https://image2.com");
|
assert_eq!(&first["link_url"], "https://image2.com");
|
||||||
|
|
||||||
let second = cap.get(1).unwrap();
|
let second = cap.get(1).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
&second[0],
|
||||||
|
"[](https://image4.com)"
|
||||||
|
);
|
||||||
assert_eq!(&second["alt"], "image3");
|
assert_eq!(&second["alt"], "image3");
|
||||||
assert_eq!(&second["img_url"], "https://image3.com");
|
assert_eq!(&second["img_url"], "https://image3.com");
|
||||||
assert_eq!(&second["link_url"], "https://image4.com");
|
assert_eq!(&second["link_url"], "https://image4.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_regex_image() {
|
||||||
|
let cap: Vec<_> = Regex::new(REGEX_IMAGE)
|
||||||
|
.unwrap()
|
||||||
|
.captures_iter(r#" "#)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let first = cap.get(0).unwrap();
|
||||||
|
assert_eq!(&first[0], "");
|
||||||
|
assert_eq!(&first["alt"], "image1");
|
||||||
|
assert_eq!(&first["url"], "https://image1.com");
|
||||||
|
|
||||||
|
let second = cap.get(1).unwrap();
|
||||||
|
assert_eq!(&second[0], "");
|
||||||
|
assert_eq!(&second["alt"], "image2");
|
||||||
|
assert_eq!(&second["url"], "https://image2.com");
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue