credence_lib/render/
renderer.rs

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