orbital_markdown/
render_html.rs1use 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#[derive(Clone, Debug, Default)]
11pub struct RenderContext<'a> {
12 pub citations: &'a [CitationRef<'a>],
13 pub attachment_urls: &'a [&'a str],
14}
15
16pub 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}