Footnotes
Some checks failed
CI / Check typos (push) Successful in 8s
CI / Check links (push) Failing after 12s
CI / Clippy (push) Successful in 54s
CI / Build and test (push) Successful in 1m11s
CI / Build container (push) Successful in 52s
CI / Deploy on waypoint (push) Successful in 44s
Some checks failed
CI / Check typos (push) Successful in 8s
CI / Check links (push) Failing after 12s
CI / Clippy (push) Successful in 54s
CI / Build and test (push) Successful in 1m11s
CI / Build container (push) Successful in 52s
CI / Deploy on waypoint (push) Successful in 44s
This commit is contained in:
169
crates/service/service-webpage/src/components/md/mod.rs
Normal file
169
crates/service/service-webpage/src/components/md/mod.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
use lazy_static::lazy_static;
|
||||
use markdown_it::generics::inline::full_link;
|
||||
use markdown_it::{MarkdownIt, Node};
|
||||
use maud::{Markup, PreEscaped, Render, html};
|
||||
use page::{Page, PageMetadata, RequestContext};
|
||||
|
||||
use crate::components::md::emote::InlineEmote;
|
||||
use crate::components::md::frontmatter::{TomlFrontMatter, YamlFrontMatter};
|
||||
use crate::components::md::link::SmartLink;
|
||||
use crate::components::md::mdx::InlineMdx;
|
||||
|
||||
mod emote;
|
||||
mod frontmatter;
|
||||
mod link;
|
||||
mod mdx;
|
||||
|
||||
lazy_static! {
|
||||
static ref MdParser: MarkdownIt = {
|
||||
let mut md = markdown_it::MarkdownIt::new();
|
||||
|
||||
{
|
||||
|
||||
use markdown_it::plugins::cmark::*;
|
||||
|
||||
inline::newline::add(&mut md);
|
||||
inline::escape::add(&mut md);
|
||||
inline::backticks::add(&mut md);
|
||||
inline::emphasis::add(&mut md);
|
||||
|
||||
// Replaced with smart links
|
||||
//inline::link::add(&mut md);
|
||||
full_link::add::<false>(&mut md, |href, title| {
|
||||
Node::new(SmartLink {
|
||||
url: href.unwrap_or_default(),
|
||||
title,
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
inline::image::add(&mut md);
|
||||
inline::autolink::add(&mut md);
|
||||
inline::entity::add(&mut md);
|
||||
|
||||
block::code::add(&mut md);
|
||||
block::fence::add(&mut md);
|
||||
block::blockquote::add(&mut md);
|
||||
block::hr::add(&mut md);
|
||||
block::list::add(&mut md);
|
||||
block::reference::add(&mut md);
|
||||
block::heading::add(&mut md);
|
||||
block::lheading::add(&mut md);
|
||||
block::paragraph::add(&mut md);
|
||||
|
||||
}
|
||||
|
||||
{
|
||||
markdown_it::plugins::html::add(&mut md);
|
||||
md_footnote::add(&mut md);
|
||||
}
|
||||
|
||||
|
||||
md.block.add_rule::<YamlFrontMatter>().before_all();
|
||||
md.block.add_rule::<TomlFrontMatter>().before_all();
|
||||
|
||||
md.inline.add_rule::<InlineEmote>();
|
||||
md.inline.add_rule::<InlineMdx>();
|
||||
|
||||
md
|
||||
};
|
||||
}
|
||||
|
||||
pub struct Markdown<'a>(pub &'a str);
|
||||
|
||||
impl Render for Markdown<'_> {
|
||||
fn render(&self) -> Markup {
|
||||
let md = Self::parse(self.0);
|
||||
let html = md.render();
|
||||
return PreEscaped(html);
|
||||
}
|
||||
}
|
||||
|
||||
impl Markdown<'_> {
|
||||
pub fn parse(md_str: &str) -> Node {
|
||||
MdParser.parse(md_str)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// MARK: helpers
|
||||
//
|
||||
|
||||
/// Try to read page metadata from a markdown file's frontmatter.
|
||||
/// - returns `none` if there is no frontmatter
|
||||
/// - returns an error if we fail to parse frontmatter
|
||||
pub fn meta_from_markdown(root_node: &Node) -> Result<Option<PageMetadata>, toml::de::Error> {
|
||||
root_node
|
||||
.children
|
||||
.first()
|
||||
.and_then(|x| x.cast::<TomlFrontMatter>())
|
||||
.map(|x| toml::from_str::<PageMetadata>(&x.content))
|
||||
.map_or(Ok(None), |v| v.map(Some))
|
||||
}
|
||||
|
||||
pub fn page_from_markdown(md: impl Into<String>, default_image: Option<String>) -> Page {
|
||||
let md: String = md.into();
|
||||
let md = Markdown::parse(&md);
|
||||
|
||||
let mut meta = meta_from_markdown(&md)
|
||||
.unwrap_or(Some(PageMetadata {
|
||||
title: "Invalid frontmatter!".into(),
|
||||
..Default::default()
|
||||
}))
|
||||
.unwrap_or_default();
|
||||
|
||||
if meta.image.is_none() {
|
||||
meta.image = default_image
|
||||
}
|
||||
|
||||
let html = PreEscaped(md.render());
|
||||
|
||||
Page {
|
||||
meta,
|
||||
generate_html: Box::new(move |page, ctx| {
|
||||
let html = html.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
html! {
|
||||
@if let Some(backlinks) = backlinks(page, ctx) {
|
||||
(backlinks)
|
||||
}
|
||||
|
||||
(html)
|
||||
}
|
||||
})
|
||||
}),
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn backlinks(page: &Page, ctx: &RequestContext) -> Option<Markup> {
|
||||
let mut last = None;
|
||||
let mut backlinks = vec![("/", "home")];
|
||||
|
||||
if page.meta.backlinks.unwrap_or(false) {
|
||||
let mut segments = ctx.route.split("/").skip(1).collect::<Vec<_>>();
|
||||
last = segments.pop();
|
||||
|
||||
let mut end = 0;
|
||||
for s in segments {
|
||||
end += s.len();
|
||||
backlinks.push((&ctx.route[0..=end], s));
|
||||
end += 1; // trailing slash
|
||||
}
|
||||
}
|
||||
|
||||
last.map(|last| {
|
||||
html! {
|
||||
div {
|
||||
@for (url, text) in backlinks {
|
||||
a href=(url) style="padding-left:5pt;padding-right:5pt;" { (text) }
|
||||
"/"
|
||||
}
|
||||
|
||||
span style="color:var(--metaColor);padding-left:5pt;padding-right:5pt;" { (last) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user