ephemeris_documents/markdown/
render.rs1use comrak::nodes::{
4 Ast, LineColumn, ListType, NodeCode, NodeCodeBlock, NodeHeading, NodeLink, NodeList, NodeValue,
5};
6use comrak::{Arena, Options, format_commonmark};
7
8use super::ast::{MarkdownBlock, MarkdownInline};
9use crate::error::{DocumentError, InternalError, MarkdownError};
10
11impl MarkdownBlock {
12 pub(super) fn render_all(blocks: &[Self]) -> Result<String, DocumentError> {
18 let arena = Arena::new();
19 let tree = Tree { arena: &arena };
20 let root = tree.node(NodeValue::Document);
21 tree.append_blocks(root, blocks)?;
22 root.validate()
23 .map_err(|validation| InternalError::markdown_tree(&validation))?;
24 let mut options = Options::default();
25 options.render.prefer_fenced = true;
26 let mut output = String::new();
27 format_commonmark(root, &options, &mut output)
28 .map_err(|source| MarkdownError::RenderFailed { source })?;
29 Ok(output)
30 }
31}
32
33struct Tree<'a> {
35 arena: &'a Arena<'a>,
36}
37
38impl<'a> Tree<'a> {
39 fn node(&self, value: NodeValue) -> comrak::Node<'a> {
40 self.arena
41 .alloc(Ast::new(value, LineColumn::default()).into())
42 }
43
44 fn append_blocks(
45 &self,
46 parent: comrak::Node<'a>,
47 blocks: &[MarkdownBlock],
48 ) -> Result<(), DocumentError> {
49 for block in blocks {
50 match block {
51 MarkdownBlock::Heading { level, content } => {
52 let heading = self.node(NodeValue::Heading(NodeHeading {
53 level: level.get(),
54 ..Default::default()
55 }));
56 self.append_inlines(heading, content);
57 parent.append(heading);
58 }
59 MarkdownBlock::Paragraph { content } => {
60 let paragraph = self.node(NodeValue::Paragraph);
61 self.append_inlines(paragraph, content);
62 parent.append(paragraph);
63 }
64 MarkdownBlock::UnorderedList { items } => {
65 let metadata = NodeList {
66 list_type: ListType::Bullet,
67 bullet_char: b'-',
68 tight: true,
69 ..Default::default()
70 };
71 let list = self.node(NodeValue::List(metadata));
72 for item in items {
73 let item_node = self.node(NodeValue::Item(metadata));
74 self.append_blocks(item_node, item)?;
75 list.append(item_node);
76 }
77 parent.append(list);
78 }
79 MarkdownBlock::CodeBlock { language, value } => {
80 if language
81 .as_deref()
82 .is_some_and(|info| info.contains(['\r', '\n']))
83 {
84 return Err(MarkdownError::CodeBlockLanguageMultiline.into());
85 }
86 let mut literal = value.clone();
87 if !literal.ends_with('\n') {
88 literal.push('\n');
89 }
90 parent.append(self.node(NodeValue::CodeBlock(Box::new(NodeCodeBlock {
91 fenced: true,
92 fence_char: b'`',
93 fence_length: 3,
94 info: language.clone().unwrap_or_default(),
95 literal,
96 ..Default::default()
97 }))));
98 }
99 }
100 }
101 Ok(())
102 }
103
104 fn append_inlines(&self, parent: comrak::Node<'a>, inlines: &[MarkdownInline]) {
105 for inline in inlines {
106 match inline {
107 MarkdownInline::Text { value } => {
108 parent.append(self.node(NodeValue::Text(value.clone().into())));
109 }
110 MarkdownInline::Emphasis { content } => {
111 let emphasis = self.node(NodeValue::Emph);
112 self.append_inlines(emphasis, content);
113 parent.append(emphasis);
114 }
115 MarkdownInline::Strong { content } => {
116 let strong = self.node(NodeValue::Strong);
117 self.append_inlines(strong, content);
118 parent.append(strong);
119 }
120 MarkdownInline::Code { value } => {
121 parent.append(self.node(NodeValue::Code(NodeCode {
122 literal: value.clone(),
123 ..Default::default()
124 })));
125 }
126 MarkdownInline::Link {
127 destination,
128 content,
129 } => {
130 let link = self.node(NodeValue::Link(Box::new(NodeLink {
131 url: destination.clone(),
132 title: String::new(),
133 })));
134 self.append_inlines(link, content);
135 parent.append(link);
136 }
137 }
138 }
139 }
140}
141
142#[cfg(test)]
143mod tests;