credence_lib/render/
renderer.rs

1use super::{context::*, preparer::*};
2
3use {
4    ::axum::http::*,
5    bytestring::*,
6    compris::*,
7    kutil_http::*,
8    kutil_std::*,
9    markdown::{mdast::*, *},
10    std::result::Result,
11};
12
13//
14// Renderer
15//
16
17/// Renderer.
18#[derive(Clone, Copy, Debug, Default, Display, FromStr)]
19#[from_str(lowercase)]
20pub enum Renderer {
21    /// Passthrough.
22    Passthrough,
23
24    /// Markdown.
25    #[strings("markdown", "md")]
26    Markdown,
27
28    /// GitHub-Flavored Markdown.
29    #[default]
30    GFM,
31}
32
33impl Renderer {
34    /// Render.
35    pub async fn render(&self, content: &str) -> Result<ByteString, StatusCode> {
36        match self {
37            Self::Passthrough => Ok(content.into()),
38            Self::Markdown => Self::render_markdown(content, Options::default()),
39            Self::GFM => Self::render_markdown(content, Options::gfm()),
40        }
41    }
42
43    /// Title from content.
44    pub fn title_from_content(&self, content: &str) -> Result<Option<ByteString>, StatusCode> {
45        match self {
46            Self::Passthrough => Ok(self.title_from_passthrough(content)?.map(|title| title.into())),
47            Self::Markdown => self.title_from_markdown(content, &ParseOptions::default()),
48            Self::GFM => self.title_from_markdown(content, &ParseOptions::gfm()),
49        }
50    }
51
52    /// Render Markdown.
53    pub fn render_markdown(content: &str, mut options: Options) -> Result<ByteString, StatusCode> {
54        options.compile.allow_dangerous_html = true;
55        to_html_with_options(content, &options).map(|html| html.into()).map_err_internal_server("Markdown to HTML")
56    }
57
58    /// Title from passthrough content.
59    ///
60    /// This will be the first `<h1>`.
61    pub fn title_from_passthrough<'content>(
62        &self,
63        content: &'content str,
64    ) -> Result<Option<&'content str>, StatusCode> {
65        if let Some(heading_start) = content.find("<h1>") {
66            let title = &content[heading_start + 4..];
67            if let Some(heading_end) = title.find("</h1>") {
68                let title = &title[..heading_end];
69                return Ok(Some(title));
70            }
71        }
72
73        Ok(None)
74    }
75
76    /// Title from Markdown content.
77    ///
78    /// This will be the text in the first heading.
79    pub fn title_from_markdown(&self, content: &str, options: &ParseOptions) -> Result<Option<ByteString>, StatusCode> {
80        // TODO: other Markdown parsers might be more efficient here
81        // allowing us *not* to parse the entire content in order to get the first heading
82
83        let node = to_mdast(content, options).map_err_internal_server("parse Markdown")?;
84
85        if let Some(children) = node.children() {
86            for child in children {
87                if let Node::Heading(heading) = child {
88                    if let Some(child) = heading.children.get(0) {
89                        if let Node::Text(text) = child {
90                            return Ok(Some(text.value.clone().into()));
91                        }
92                    }
93                }
94            }
95        }
96
97        Ok(None)
98    }
99}
100
101impl_resolve_from_str!(Renderer);
102
103impl RenderPreparer for Renderer {
104    async fn prepare<'own>(&self, context: &mut RenderContext<'own>) -> Result<(), StatusCode> {
105        if !context.variables.contains_key("content") {
106            if let Some(content) = context.rendered_page.content.as_ref() {
107                let content = self.render(content).await?;
108                context.variables.insert("content".into(), content.into());
109            }
110        }
111
112        Ok(())
113    }
114}