use std::ops::Range;
use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd, html, utils::TextMergeWithOffset};
pub(crate) fn title_from_markdown(body: &str) -> &str {
let line = body
.lines()
.find(|line| !line.trim().is_empty())
.unwrap_or_default();
atx_heading_title(line).unwrap_or_else(|| line.trim())
}
fn atx_heading_title(line: &str) -> Option<&str> {
let mut events = TextMergeWithOffset::new(Parser::new(line).into_offset_iter());
let Some((Event::Start(Tag::Heading { .. }), _)) = events.next() else {
return None;
};
let mut content_range: Option<Range<usize>> = None;
for (event, range) in events {
if matches!(event, Event::End(TagEnd::Heading(_))) {
let content = match content_range {
Some(range) => &line[range],
None => "",
};
return Some(content);
}
content_range = Some(match content_range {
Some(content_range) => {
content_range.start.min(range.start)..content_range.end.max(range.end)
}
None => range,
});
}
None
}
pub(crate) fn render_markdown(text: &str) -> String {
let mut options = Options::empty();
options.insert(Options::ENABLE_FOOTNOTES);
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_TASKLISTS);
let parser = Parser::new_ext(text, options);
let mut html = String::new();
html::push_html(&mut html, parser);
html
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_title_from_markdown() {
assert_eq!(title_from_markdown("# Foo"), "Foo");
assert_eq!(title_from_markdown("## Foo"), "Foo");
assert_eq!(title_from_markdown(" # Foo"), "Foo");
assert_eq!(title_from_markdown("#\tFoo"), "Foo");
assert_eq!(title_from_markdown("# Foo #"), "Foo");
assert_eq!(title_from_markdown("# Foo ##"), "Foo");
assert_eq!(title_from_markdown("# Foo#"), "Foo#");
assert_eq!(title_from_markdown("# *Foo*"), "*Foo*");
assert_eq!(title_from_markdown("# [Foo](bar)"), "[Foo](bar)");
assert_eq!(title_from_markdown("# #"), "");
assert_eq!(title_from_markdown("#"), "");
assert_eq!(title_from_markdown("#5 bolt"), "#5 bolt");
assert_eq!(title_from_markdown("###Foo"), "###Foo");
assert_eq!(title_from_markdown(" # Foo"), "# Foo");
assert_eq!(title_from_markdown("Foo"), "Foo");
assert_eq!(title_from_markdown(""), "");
}
#[test]
fn test_markdown_enables_extensions() {
assert!(render_markdown("~~gone~~").contains("<del>gone</del>"));
assert!(render_markdown("| head |\n| ---- |\n| body |").contains("<table>"));
assert!(render_markdown("- [x] done").contains("type=\"checkbox\""));
let footnotes = render_markdown("Text[^1]\n\n[^1]: note");
assert!(footnotes.contains("footnote-reference"));
assert!(footnotes.contains("footnote-definition"));
}
}