pub use pulldown_cmark::{CowStr, Options};
use pulldown_cmark::{Event, LinkType, Parser};
use core::ops::Range;
use std::collections::BTreeMap;
mod render;
use render::Renderer;
mod component;
pub struct ElementAttributes<H> {
pub classes: Vec<String>,
pub style: Option<String>,
pub on_click: Option<H>,
}
impl<H> Default for ElementAttributes<H> {
fn default() -> Self {
Self {
style: None,
classes: vec![],
on_click: None,
}
}
}
pub enum HtmlElement {
Div,
Span,
Paragraph,
BlockQuote,
Ul,
Ol(i32),
Li,
Heading(u8),
Table,
Thead,
Trow,
Tcell,
Italics,
Bold,
StrikeThrough,
Pre,
Code,
}
pub trait Context<'a, 'callback>: Copy + 'a
where
'callback: 'a,
{
type View: Clone + 'callback;
type Handler<T: 'callback>: 'callback;
type MouseEvent: 'static;
fn props(self) -> MarkdownProps;
fn set_frontmatter(&mut self, frontmatter: String);
fn render_links(self, link: LinkDescription<Self::View>) -> Result<Self::View, String>;
fn call_handler<T>(callback: &Self::Handler<T>, input: T);
fn make_md_handler(
self,
position: Range<usize>,
stop_propagation: bool,
) -> Self::Handler<Self::MouseEvent>;
#[cfg(feature = "debug")]
fn send_debug_info(self, info: Vec<String>);
fn el_with_attributes(
self,
e: HtmlElement,
inside: Self::View,
attributes: ElementAttributes<Self::Handler<Self::MouseEvent>>,
) -> Self::View;
fn el(self, e: HtmlElement, inside: Self::View) -> Self::View {
self.el_with_attributes(e, inside, Default::default())
}
fn el_span_with_inner_html(
self,
inner_html: String,
attributes: ElementAttributes<Self::Handler<Self::MouseEvent>>,
) -> Self::View;
fn el_hr(self, attributes: ElementAttributes<Self::Handler<Self::MouseEvent>>) -> Self::View;
fn el_br(self) -> Self::View;
fn el_fragment(self, children: Vec<Self::View>) -> Self::View;
fn el_a(self, children: Self::View, href: String) -> Self::View;
fn el_img(self, src: String, alt: String) -> Self::View;
fn el_empty(self) -> Self::View {
self.el_fragment(vec![])
}
fn el_text(self, text: CowStr<'a>) -> Self::View;
fn el_input_checkbox(
self,
checked: bool,
attributes: ElementAttributes<Self::Handler<Self::MouseEvent>>,
) -> Self::View;
fn mount_dynamic_link(self, rel: &str, href: &str, integrity: &str, crossorigin: &str);
fn has_custom_component(self, name: &str) -> bool;
fn render_custom_component(
self,
name: &str,
input: MdComponentProps<Self::View>,
) -> Result<Self::View, ComponentCreationError>;
fn render_tasklist_marker(self, m: bool, position: Range<usize>) -> Self::View {
let attributes = ElementAttributes {
on_click: Some(self.make_md_handler(position, true)),
..Default::default()
};
self.el_input_checkbox(m, attributes)
}
fn render_rule(self, range: Range<usize>) -> Self::View {
let attributes = ElementAttributes {
on_click: Some(self.make_md_handler(range, false)),
..Default::default()
};
self.el_hr(attributes)
}
fn render_code(self, s: CowStr<'a>, range: Range<usize>) -> Self::View {
let callback = self.make_md_handler(range.clone(), false);
let attributes = ElementAttributes {
on_click: Some(callback),
..Default::default()
};
self.el_with_attributes(HtmlElement::Code, self.el_text(s), attributes)
}
fn render_text(self, s: CowStr<'a>, range: Range<usize>) -> Self::View {
let callback = self.make_md_handler(range, false);
let attributes = ElementAttributes {
on_click: Some(callback),
..Default::default()
};
self.el_with_attributes(HtmlElement::Span, self.el_text(s), attributes)
}
fn has_custom_links(self) -> bool;
fn render_link(self, link: LinkDescription<Self::View>) -> Result<Self::View, String> {
if self.has_custom_links() {
self.render_links(link)
} else {
Ok(if link.image {
self.el_img(link.url, link.title)
} else {
self.el_a(link.content, link.url)
})
}
}
}
pub struct LinkDescription<V> {
pub url: String,
pub content: V,
pub title: String,
pub link_type: LinkType,
pub image: bool,
}
pub enum HtmlError {
NotImplemented(String),
Link(String),
Syntax(String),
CustomComponent { name: String, msg: String },
Math,
}
#[derive(PartialEq)]
pub struct MdComponentProps<V> {
pub attributes: BTreeMap<String, String>,
pub children: V,
}
impl<V> MdComponentProps<V> {
pub fn get(&self, name: &str) -> Option<String> {
self.attributes.get(name).cloned()
}
pub fn get_parsed<T>(&self, name: &str) -> Result<T, String>
where
T: std::str::FromStr,
T::Err: core::fmt::Debug,
{
match self.attributes.get(name) {
Some(x) => x.clone().parse().map_err(|e| format!("{e:?}")),
None => Err(format!("please provide the attribute `{name}`")),
}
}
pub fn get_parsed_optional<T>(&self, name: &str) -> Result<Option<T>, String>
where
T: std::str::FromStr,
T::Err: core::fmt::Debug,
{
match self.attributes.get(name) {
Some(x) => match x.parse() {
Ok(a) => Ok(Some(a)),
Err(e) => Err(format!("{e:?}")),
},
None => Ok(None),
}
}
}
pub struct ComponentCreationError(String);
impl<T: std::fmt::Debug> From<T> for ComponentCreationError {
fn from(value: T) -> Self {
Self(format!("{:?}", value))
}
}
pub struct MarkdownProps {
pub hard_line_breaks: bool,
pub wikilinks: bool,
pub parse_options: Option<pulldown_cmark::Options>,
pub theme: Option<&'static str>,
}
pub fn render_markdown<'a, 'callback, F: Context<'a, 'callback>>(
cx: F,
source: &'a str,
) -> F::View {
let parse_options_default = Options::ENABLE_GFM
| Options::ENABLE_MATH
| Options::ENABLE_TABLES
| Options::ENABLE_TASKLISTS
| Options::ENABLE_WIKILINKS
| Options::ENABLE_STRIKETHROUGH
| Options::ENABLE_YAML_STYLE_METADATA_BLOCKS;
let options = cx.props().parse_options.unwrap_or(parse_options_default);
let mut stream: Vec<_> = Parser::new_ext(source, options)
.into_offset_iter()
.collect();
#[cfg(feature = "debug")]
{
let debug_info: Vec<String> = stream.iter().map(|x| format!("{:?}", x)).collect();
cx.send_debug_info(debug_info)
}
if cx.props().hard_line_breaks {
for (r, _) in &mut stream {
if *r == Event::SoftBreak {
*r = Event::HardBreak
}
}
}
let elements = Renderer::new(cx, &mut stream.into_iter()).collect::<Vec<_>>();
cx.mount_dynamic_link(
"stylesheet",
"https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/katex.min.css",
"sha384-3UiQGuEI4TTMaFmGIZumfRPtfKQ3trwQE2JgosJxCnGmQpL/lJdjpcHkaaFwHlcI",
"anonymous",
);
cx.el_fragment(elements)
}