nargo_document/generator/
markdown.rs1use oak_core::{Builder, ParseSession};
5use oak_markdown::{
6 ast::{Block, Blockquote, CodeBlock, Heading, Html, Inline, List, ListItem, MarkdownRoot, Paragraph, Table, TableCell},
7 MarkdownBuilder, MarkdownLanguage,
8};
9
10#[derive(Debug, Clone)]
12pub struct MarkdownRendererConfig {
13 pub enable_tables: bool,
15 pub enable_footnotes: bool,
17 pub enable_strikethrough: bool,
19 pub enable_tasklists: bool,
21 pub enable_smart_punctuation: bool,
23}
24
25impl Default for MarkdownRendererConfig {
26 fn default() -> Self {
27 Self { enable_tables: true, enable_footnotes: true, enable_strikethrough: true, enable_tasklists: true, enable_smart_punctuation: true }
28 }
29}
30
31#[derive(Debug, Clone)]
33pub struct MarkdownRenderer {
34 config: MarkdownRendererConfig,
36 lang_config: MarkdownLanguage,
38}
39
40impl MarkdownRenderer {
41 pub fn new() -> Self {
43 Self { config: MarkdownRendererConfig::default(), lang_config: MarkdownLanguage::default() }
44 }
45
46 pub fn with_config(config: MarkdownRendererConfig) -> Self {
52 let mut lang_config = MarkdownLanguage::default();
53 lang_config.allow_tables = config.enable_tables;
54 lang_config.allow_footnotes = config.enable_footnotes;
55 lang_config.allow_strikethrough = config.enable_strikethrough;
56 lang_config.allow_task_lists = config.enable_tasklists;
57
58 Self { config, lang_config }
59 }
60
61 pub fn config(&self) -> &MarkdownRendererConfig {
63 &self.config
64 }
65
66 pub fn config_mut(&mut self) -> &mut MarkdownRendererConfig {
68 &mut self.config
69 }
70
71 pub fn render(&self, markdown: &str) -> Result<String, std::io::Error> {
81 Ok(self.render_fallback(markdown))
82 }
83
84 fn render_ast(&self, root: &MarkdownRoot) -> String {
94 let mut html = String::new();
95
96 for block in &root.blocks {
97 html.push_str(&self.render_block(block));
98 }
99
100 html
101 }
102
103 fn render_block(&self, block: &Block) -> String {
113 match block {
114 Block::Heading(heading) => self.render_heading(heading),
115 Block::Paragraph(paragraph) => self.render_paragraph(paragraph),
116 Block::CodeBlock(code_block) => self.render_code_block(code_block),
117 Block::List(list) => self.render_list(list),
118 Block::Blockquote(blockquote) => self.render_blockquote(blockquote),
119 Block::HorizontalRule(_) => "<hr />\n".to_string(),
120 Block::Table(table) => self.render_table(table),
121 Block::Html(html) => self.render_html(html),
122 Block::AbbreviationDefinition(_) => String::new(),
123 }
124 }
125
126 fn render_heading(&self, heading: &Heading) -> String {
136 let tag = format!("h{}", heading.level);
137 let escaped_content = self.escape_html(&heading.content);
138 format!("<{}>{}</{}>\n", tag, escaped_content, tag)
139 }
140
141 fn render_paragraph(&self, paragraph: &Paragraph) -> String {
151 let escaped_content = self.escape_html(¶graph.content);
152 format!("<p>{}</p>\n", escaped_content)
153 }
154
155 fn render_code_block(&self, code_block: &CodeBlock) -> String {
165 let class = if let Some(lang) = &code_block.language { format!(" class=\"language-{}\"", self.escape_html(lang)) } else { String::new() };
166 let escaped_content = self.escape_html(&code_block.content);
167 format!("<pre><code{}>{}</code></pre>\n", class, escaped_content)
168 }
169
170 fn render_list(&self, list: &List) -> String {
180 let tag = if list.is_ordered { "ol" } else { "ul" };
181 let mut html = format!("<{}>\n", tag);
182
183 for item in &list.items {
184 html.push_str(&self.render_list_item(item));
185 }
186
187 html.push_str(&format!("</{}>\n", tag));
188 html
189 }
190
191 fn render_list_item(&self, list_item: &ListItem) -> String {
201 let mut html = String::from("<li>");
202
203 if list_item.is_task {
204 let checked = if list_item.is_checked.unwrap_or(false) { "checked" } else { "" };
205 html.push_str(&format!("<input type=\"checkbox\" disabled {} /> ", checked));
206 }
207
208 for block in &list_item.content {
209 html.push_str(&self.render_block(block));
210 }
211
212 html.push_str("</li>\n");
213 html
214 }
215
216 fn render_blockquote(&self, blockquote: &Blockquote) -> String {
226 let mut html = String::from("<blockquote>\n");
227
228 for block in &blockquote.content {
229 html.push_str(&self.render_block(block));
230 }
231
232 html.push_str("</blockquote>\n");
233 html
234 }
235
236 fn render_table(&self, table: &Table) -> String {
246 let mut html = String::from("<table>\n");
247
248 html.push_str("<thead>\n<tr>\n");
249 for cell in &table.header.cells {
250 html.push_str(&self.render_table_cell(cell, "th"));
251 }
252 html.push_str("</tr>\n</thead>\n");
253
254 html.push_str("<tbody>\n");
255 for row in &table.rows {
256 html.push_str("<tr>\n");
257 for cell in &row.cells {
258 html.push_str(&self.render_table_cell(cell, "td"));
259 }
260 html.push_str("</tr>\n");
261 }
262 html.push_str("</tbody>\n");
263
264 html.push_str("</table>\n");
265 html
266 }
267
268 fn render_table_cell(&self, cell: &TableCell, tag: &str) -> String {
279 let escaped_content = self.escape_html(&cell.content);
280 format!("<{}>{}</{}>\n", tag, escaped_content, tag)
281 }
282
283 fn render_html(&self, html: &Html) -> String {
293 format!("{}\n", html.content)
294 }
295
296 fn render_inline(&self, inline: &Inline) -> String {
306 match inline {
307 Inline::Text(text) => self.escape_html(text),
308 Inline::Bold(text) => format!("<strong>{}</strong>", self.escape_html(text)),
309 Inline::Italic(text) => format!("<em>{}</em>", self.escape_html(text)),
310 Inline::Code(text) => format!("<code>{}</code>", self.escape_html(text)),
311 Inline::Link { text, url, title } => {
312 let title_attr = if let Some(t) = title { format!(" title=\"{}\"", self.escape_html(t)) } else { String::new() };
313 format!("<a href=\"{}\"{}>{}</a>", self.escape_html(url), title_attr, self.escape_html(text))
314 }
315 Inline::Image { alt, url, title } => {
316 let title_attr = if let Some(t) = title { format!(" title=\"{}\"", self.escape_html(t)) } else { String::new() };
317 format!("<img src=\"{}\" alt=\"{}\"{} />", self.escape_html(url), self.escape_html(alt), title_attr)
318 }
319 Inline::Abbreviation { key, .. } => key.clone(),
320 }
321 }
322
323 fn escape_html(&self, text: &str) -> String {
333 text.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """).replace('\'', "'")
334 }
335
336 fn render_fallback(&self, markdown: &str) -> String {
346 use pulldown_cmark::{html, Options, Parser};
347
348 let mut options = Options::empty();
349
350 if self.config.enable_tables {
351 options.insert(Options::ENABLE_TABLES);
352 }
353 if self.config.enable_footnotes {
354 options.insert(Options::ENABLE_FOOTNOTES);
355 }
356 if self.config.enable_strikethrough {
357 options.insert(Options::ENABLE_STRIKETHROUGH);
358 }
359 if self.config.enable_tasklists {
360 options.insert(Options::ENABLE_TASKLISTS);
361 }
362 if self.config.enable_smart_punctuation {
363 options.insert(Options::ENABLE_SMART_PUNCTUATION);
364 }
365
366 let parser = Parser::new_ext(markdown, options);
367 let mut html_output = String::new();
368 html::push_html(&mut html_output, parser);
369
370 html_output
371 }
372}
373
374impl Default for MarkdownRenderer {
375 fn default() -> Self {
376 Self::new()
377 }
378}