Skip to main content

orbital_markdown/
render_html.rs

1use pulldown_cmark::{html, Options, Parser};
2
3use crate::citations::{replace_citation_refs, strip_duplicate_images, CitationRef};
4use crate::links::style_markdown_links;
5use crate::mentions::{prepare_mention_markdown, replace_mention_links};
6use crate::options::OrbitalMarkdownOptions;
7use crate::sanitize::sanitize_html;
8
9/// Context for resolving citation refs and deduping images.
10#[derive(Clone, Debug, Default)]
11pub struct RenderContext<'a> {
12    pub citations: &'a [CitationRef<'a>],
13    pub attachment_urls: &'a [&'a str],
14}
15
16/// Render markdown to sanitized HTML.
17pub fn render_to_html(
18    markdown: &str,
19    options: &OrbitalMarkdownOptions,
20    ctx: &RenderContext<'_>,
21) -> String {
22    if markdown.trim().is_empty() {
23        return String::new();
24    }
25
26    let mut source = if options.enable_images {
27        strip_duplicate_images(markdown, ctx.attachment_urls)
28    } else {
29        markdown.to_string()
30    };
31
32    if options.enable_mention_refs {
33        source = prepare_mention_markdown(&source);
34    }
35
36    let mut parse_options = Options::empty();
37    parse_options.insert(Options::ENABLE_STRIKETHROUGH);
38
39    let parser = Parser::new_ext(&source, parse_options);
40    let mut html_output = String::new();
41    html::push_html(&mut html_output, parser);
42
43    let mut html = html_output;
44    if options.enable_mention_refs {
45        html = replace_mention_links(&html, options.mention_style);
46    }
47
48    html = sanitize_html(&html, options.enable_images);
49
50    if options.enable_images {
51        html = html.replace(
52            "<img src=",
53            "<img class=\"orbital-markdown__image\" loading=\"lazy\" src=",
54        );
55    }
56
57    if options.enable_citation_refs {
58        html = replace_citation_refs(&html, ctx.citations, options.citation_style);
59    }
60
61    style_markdown_links(&html)
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use crate::CitationLinkStyle;
68
69    #[test]
70    fn bold_renders() {
71        let html = render_to_html(
72            "Hello **bold**",
73            &OrbitalMarkdownOptions::default(),
74            &RenderContext::default(),
75        );
76        assert!(html.contains("<strong>bold</strong>"));
77    }
78
79    #[test]
80    fn citation_ref_in_body() {
81        let citations = [CitationRef {
82            id: "cit-1",
83            display_index: 1,
84        }];
85        let ctx = RenderContext {
86            citations: &citations,
87            attachment_urls: &[],
88        };
89        let html = render_to_html(
90            "See [^cit-1] here.",
91            &OrbitalMarkdownOptions {
92                enable_citation_refs: true,
93                enable_mention_refs: false,
94                enable_images: false,
95                citation_style: CitationLinkStyle::discussion(),
96                mention_style: Default::default(),
97            },
98            &ctx,
99        );
100        assert!(html.contains("discussion-citation-ref-cit-1"));
101        assert!(html.contains("orbital-link orbital-link--inline"));
102    }
103
104    #[test]
105    fn markdown_link_uses_orbital_link_classes() {
106        let html = render_to_html(
107            "See [design doc](https://example.com).",
108            &OrbitalMarkdownOptions::default(),
109            &RenderContext::default(),
110        );
111        assert!(html.contains("orbital-link orbital-link--inline"));
112        assert!(html.contains("design doc"));
113    }
114
115    #[test]
116    fn mention_ref_in_body() {
117        let html = render_to_html(
118            "Hi @[Jordan Lee](u1)!",
119            &OrbitalMarkdownOptions {
120                enable_citation_refs: false,
121                enable_mention_refs: true,
122                enable_images: false,
123                citation_style: CitationLinkStyle::history(),
124                mention_style: crate::MentionLinkStyle::history(),
125            },
126            &RenderContext::default(),
127        );
128        assert!(html.contains("data-mention-id=\"u1\""));
129        assert!(html.contains("orbital-history__mention-ref"));
130        assert!(html.contains("orbital-link orbital-link--inline"));
131    }
132}