use lazy_static::lazy_static;
use pulldown_cmark::*;
use regex::Regex;
use tracing::debug;
lazy_static! {
static ref REPLACE: Regex = Regex::new(
r#"(#([A-Za-z0-9]+))|(@([A-Za-z0-9_-]+))|(http://\S+[[:alnum:]])|(https://\S+[[:alnum:]])"#
)
.unwrap();
}
pub struct Rendered {
pub rendered: String,
}
pub fn render_markdown(owner: &str, repo: &str, s: &str) -> Rendered {
let mut options = Options::empty();
options.insert(
Options::ENABLE_TABLES
| Options::ENABLE_FOOTNOTES
| Options::ENABLE_STRIKETHROUGH
| Options::ENABLE_TASKLISTS
| Options::ENABLE_SMART_PUNCTUATION,
);
let mut html_output = String::new();
let mut it = P {
p: Parser::new_ext(s, options),
next: [None, None, None],
owner,
repo,
current_text: None,
mentions: Vec::new(),
};
html::push_html(&mut html_output, &mut it);
Rendered {
rendered: ammonia::clean(&html_output),
}
}
struct P<'a> {
p: Parser<'a>,
next: [Option<Event<'a>>; 3],
current_text: Option<(CowStr<'a>, usize)>,
owner: &'a str,
repo: &'a str,
mentions: Vec<String>,
}
impl<'a> Iterator for P<'a> {
type Item = Event<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
debug!("{:?} {:?}", self.next, self.current_text);
if let Some(next) = self.next[0].take() {
return Some(next);
}
if let Some(next) = self.next[1].take() {
return Some(next);
}
if let Some(next) = self.next[2].take() {
return Some(next);
}
if let Some((c, i)) = self.current_text.take() {
let cap = self.make_next(&c, i);
if let Some((start, end)) = cap {
let result = if start > 0 {
let t = c.split_at(i).1.split_at(start).0;
Some(Event::Text(t.to_string().into()))
} else {
None
};
if i + end < c.len() {
self.current_text = Some((c, i + end));
}
if result.is_some() {
return result;
}
} else {
let d = c.split_at(i).1;
debug!("return {:?}", d);
return Some(Event::Text(d.to_string().into()));
}
} else {
match self.p.next() {
None => return None,
Some(Event::Html(t)) => return Some(Event::Text(t)),
Some(Event::Text(t)) => self.current_text = Some((t, 0)),
Some(e) => return Some(e),
}
}
}
}
}
impl<'a> P<'a> {
fn make_next(&mut self, c: &str, i: usize) -> Option<(usize, usize)> {
let d = c.split_at(i).1;
debug!("d = {:?}", d);
let cap = if let Some(cap) = REPLACE.captures(d) {
cap
} else {
return None;
};
debug!("cap = {:?}", cap);
let mut l = None;
if let Some(disc) = cap.get(2) {
if disc.as_str().chars().all(|c| c >= '0' && c <= '9') {
l = Some(Tag::Link {
link_type: LinkType::Inline,
dest_url: format!(
"/{}/{}/discussions/{}",
self.owner,
self.repo,
disc.as_str()
)
.into(),
title: "".into(),
id: "".into(),
});
} else {
l = Some(Tag::Link {
link_type: LinkType::Inline,
dest_url: format!("/{}/{}/changes/{}", self.owner, self.repo, disc.as_str())
.into(),
title: "".into(),
id: "".into(),
});
}
} else if let Some(link) = cap.get(4) {
l = Some(Tag::Link {
link_type: LinkType::Inline,
dest_url: format!("/{}", link.as_str()).into(),
title: "".into(),
id: "".into(),
});
self.mentions.push(link.as_str().to_string())
} else if let Some(http) = cap.get(5) {
l = Some(Tag::Link {
link_type: LinkType::Inline,
dest_url: http.as_str().to_string().into(),
title: "".into(),
id: "".into(),
});
} else if let Some(http) = cap.get(6) {
l = Some(Tag::Link {
link_type: LinkType::Inline,
dest_url: http.as_str().to_string().into(),
title: "".into(),
id: "".into(),
});
}
self.next[0] = l.clone().map(Event::Start);
self.next[1] = Some(Event::Text(cap.get(0).unwrap().as_str().to_string().into()));
self.next[2] = Some(Event::End(TagEnd::Link));
let z = cap.get(0).unwrap();
Some((z.start(), z.end()))
}
}