use pulldown_cmark::{html, Options, Parser};
use crate::{BlockNode, Diagnostic, Document, ErrorCode, InlineExt, Level, MdEvent, Node};
use super::{args::parse_inline_exts, EvalContext, Forge};
pub trait HtmlRenderer {
fn render_block(&self, block: &BlockNode, ctx: &EvalContext, children_html: String) -> String;
fn render_inline(&self, inline: &InlineExt, ctx: &EvalContext) -> String;
}
pub(super) fn render_html_document(
forge: &Forge,
doc: &Document,
ctx: &EvalContext,
renderer: &dyn HtmlRenderer,
) -> Result<String, Vec<Diagnostic>> {
let mut diagnostics = Vec::new();
let html = render_nodes(forge, &doc.nodes, ctx, renderer, &mut diagnostics);
if diagnostics.is_empty() {
Ok(html)
} else {
Err(diagnostics)
}
}
fn render_nodes(
forge: &Forge,
nodes: &[Node],
ctx: &EvalContext,
renderer: &dyn HtmlRenderer,
diagnostics: &mut Vec<Diagnostic>,
) -> String {
let mut out = String::new();
for node in nodes {
match node {
Node::Markdown(events) => {
for event in events {
let MdEvent::Text(text) = event;
out.push_str(&render_markdown_text(
forge,
text,
ctx,
renderer,
diagnostics,
));
}
}
Node::Block(block) => {
if forge.blocks.iter().any(|b| b.name == block.name) {
let children = render_nodes(forge, &block.body, ctx, renderer, diagnostics);
out.push_str(&renderer.render_block(block, ctx, children));
} else {
diagnostics.push(Diagnostic {
level: Level::Error,
code: ErrorCode::UnknownBlock,
message: format!("unknown block '{}'", block.name),
span: block.span.clone(),
suggestion: None,
});
}
}
}
}
out
}
fn render_markdown_text(
forge: &Forge,
text: &str,
ctx: &EvalContext,
renderer: &dyn HtmlRenderer,
diagnostics: &mut Vec<Diagnostic>,
) -> String {
let mut markdown = String::new();
let mut replacements = Vec::new();
let mut last = 0;
for (index, inline) in parse_inline_exts(text).into_iter().enumerate() {
let local_start = inline.span.start;
let local_end = inline.span.end;
if local_start > last {
markdown.push_str(&text[last..local_start]);
}
let placeholder = format!("%%MDFORGE_INLINE_{}%%", index);
markdown.push_str(&placeholder);
if forge.inlines.iter().any(|i| i.name == inline.name) {
replacements.push((placeholder, renderer.render_inline(&inline, ctx)));
} else {
diagnostics.push(Diagnostic {
level: Level::Error,
code: ErrorCode::UnknownInline,
message: format!("unknown inline '{}'", inline.name),
span: inline.span.clone(),
suggestion: None,
});
replacements.push((placeholder, escape_html(&text[local_start..local_end])));
}
last = local_end;
}
if last < text.len() {
markdown.push_str(&text[last..]);
}
let mut out = markdown_to_html(&markdown);
for (placeholder, html) in replacements {
out = out.replace(&placeholder, &html);
}
out
}
fn markdown_to_html(markdown: &str) -> String {
let parser = Parser::new_ext(markdown, Options::all());
let mut out = String::new();
html::push_html(&mut out, parser);
out
}
fn escape_html(value: &str) -> String {
value
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}