use oak_core::{Builder, ParseSession};
use oak_markdown::{
ast::{Block, Blockquote, CodeBlock, Heading, Html, Inline, List, ListItem, MarkdownRoot, Paragraph, Table, TableCell},
MarkdownBuilder, MarkdownLanguage,
};
#[derive(Debug, Clone)]
pub struct MarkdownRendererConfig {
pub enable_tables: bool,
pub enable_footnotes: bool,
pub enable_strikethrough: bool,
pub enable_tasklists: bool,
pub enable_smart_punctuation: bool,
}
impl Default for MarkdownRendererConfig {
fn default() -> Self {
Self { enable_tables: true, enable_footnotes: true, enable_strikethrough: true, enable_tasklists: true, enable_smart_punctuation: true }
}
}
#[derive(Debug, Clone)]
pub struct MarkdownRenderer {
config: MarkdownRendererConfig,
lang_config: MarkdownLanguage,
}
impl MarkdownRenderer {
pub fn new() -> Self {
Self { config: MarkdownRendererConfig::default(), lang_config: MarkdownLanguage::default() }
}
pub fn with_config(config: MarkdownRendererConfig) -> Self {
let mut lang_config = MarkdownLanguage::default();
lang_config.allow_tables = config.enable_tables;
lang_config.allow_footnotes = config.enable_footnotes;
lang_config.allow_strikethrough = config.enable_strikethrough;
lang_config.allow_task_lists = config.enable_tasklists;
Self { config, lang_config }
}
pub fn config(&self) -> &MarkdownRendererConfig {
&self.config
}
pub fn config_mut(&mut self) -> &mut MarkdownRendererConfig {
&mut self.config
}
pub fn render(&self, markdown: &str) -> Result<String, std::io::Error> {
Ok(self.render_fallback(markdown))
}
fn render_ast(&self, root: &MarkdownRoot) -> String {
let mut html = String::new();
for block in &root.blocks {
html.push_str(&self.render_block(block));
}
html
}
fn render_block(&self, block: &Block) -> String {
match block {
Block::Heading(heading) => self.render_heading(heading),
Block::Paragraph(paragraph) => self.render_paragraph(paragraph),
Block::CodeBlock(code_block) => self.render_code_block(code_block),
Block::List(list) => self.render_list(list),
Block::Blockquote(blockquote) => self.render_blockquote(blockquote),
Block::HorizontalRule(_) => "<hr />\n".to_string(),
Block::Table(table) => self.render_table(table),
Block::Html(html) => self.render_html(html),
Block::AbbreviationDefinition(_) => String::new(),
}
}
fn render_heading(&self, heading: &Heading) -> String {
let tag = format!("h{}", heading.level);
let escaped_content = self.escape_html(&heading.content);
format!("<{}>{}</{}>\n", tag, escaped_content, tag)
}
fn render_paragraph(&self, paragraph: &Paragraph) -> String {
let escaped_content = self.escape_html(¶graph.content);
format!("<p>{}</p>\n", escaped_content)
}
fn render_code_block(&self, code_block: &CodeBlock) -> String {
let class = if let Some(lang) = &code_block.language { format!(" class=\"language-{}\"", self.escape_html(lang)) } else { String::new() };
let escaped_content = self.escape_html(&code_block.content);
format!("<pre><code{}>{}</code></pre>\n", class, escaped_content)
}
fn render_list(&self, list: &List) -> String {
let tag = if list.is_ordered { "ol" } else { "ul" };
let mut html = format!("<{}>\n", tag);
for item in &list.items {
html.push_str(&self.render_list_item(item));
}
html.push_str(&format!("</{}>\n", tag));
html
}
fn render_list_item(&self, list_item: &ListItem) -> String {
let mut html = String::from("<li>");
if list_item.is_task {
let checked = if list_item.is_checked.unwrap_or(false) { "checked" } else { "" };
html.push_str(&format!("<input type=\"checkbox\" disabled {} /> ", checked));
}
for block in &list_item.content {
html.push_str(&self.render_block(block));
}
html.push_str("</li>\n");
html
}
fn render_blockquote(&self, blockquote: &Blockquote) -> String {
let mut html = String::from("<blockquote>\n");
for block in &blockquote.content {
html.push_str(&self.render_block(block));
}
html.push_str("</blockquote>\n");
html
}
fn render_table(&self, table: &Table) -> String {
let mut html = String::from("<table>\n");
html.push_str("<thead>\n<tr>\n");
for cell in &table.header.cells {
html.push_str(&self.render_table_cell(cell, "th"));
}
html.push_str("</tr>\n</thead>\n");
html.push_str("<tbody>\n");
for row in &table.rows {
html.push_str("<tr>\n");
for cell in &row.cells {
html.push_str(&self.render_table_cell(cell, "td"));
}
html.push_str("</tr>\n");
}
html.push_str("</tbody>\n");
html.push_str("</table>\n");
html
}
fn render_table_cell(&self, cell: &TableCell, tag: &str) -> String {
let escaped_content = self.escape_html(&cell.content);
format!("<{}>{}</{}>\n", tag, escaped_content, tag)
}
fn render_html(&self, html: &Html) -> String {
format!("{}\n", html.content)
}
fn render_inline(&self, inline: &Inline) -> String {
match inline {
Inline::Text(text) => self.escape_html(text),
Inline::Bold(text) => format!("<strong>{}</strong>", self.escape_html(text)),
Inline::Italic(text) => format!("<em>{}</em>", self.escape_html(text)),
Inline::Code(text) => format!("<code>{}</code>", self.escape_html(text)),
Inline::Link { text, url, title } => {
let title_attr = if let Some(t) = title { format!(" title=\"{}\"", self.escape_html(t)) } else { String::new() };
format!("<a href=\"{}\"{}>{}</a>", self.escape_html(url), title_attr, self.escape_html(text))
}
Inline::Image { alt, url, title } => {
let title_attr = if let Some(t) = title { format!(" title=\"{}\"", self.escape_html(t)) } else { String::new() };
format!("<img src=\"{}\" alt=\"{}\"{} />", self.escape_html(url), self.escape_html(alt), title_attr)
}
Inline::Abbreviation { key, .. } => key.clone(),
}
}
fn escape_html(&self, text: &str) -> String {
text.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """).replace('\'', "'")
}
fn render_fallback(&self, markdown: &str) -> String {
use pulldown_cmark::{html, Options, Parser};
let mut options = Options::empty();
if self.config.enable_tables {
options.insert(Options::ENABLE_TABLES);
}
if self.config.enable_footnotes {
options.insert(Options::ENABLE_FOOTNOTES);
}
if self.config.enable_strikethrough {
options.insert(Options::ENABLE_STRIKETHROUGH);
}
if self.config.enable_tasklists {
options.insert(Options::ENABLE_TASKLISTS);
}
if self.config.enable_smart_punctuation {
options.insert(Options::ENABLE_SMART_PUNCTUATION);
}
let parser = Parser::new_ext(markdown, options);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
html_output
}
}
impl Default for MarkdownRenderer {
fn default() -> Self {
Self::new()
}
}