use std::path::Path; use image::image_dimensions; use indoc::formatdoc; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd}; use syntect::{highlighting::ThemeSet, html::highlighted_html_for_string, parsing::SyntaxSet}; use tracing::{debug, error}; use crate::picture_generator::{ picture_markup_generator::generate_picture_markup, resolutions::get_max_resolution, }; pub const MAX_BLOG_IMAGE_RESOLUTION: (u32, u32) = (1280, 860); enum TextKind { Text, Heading(Option), Code(String), } // pub fn parse_markdown(markdown: &str) -> ::askama::Result pub fn parse_markdown(markdown: &str) -> ::askama::Result { let mut options = Options::empty(); options.insert(Options::ENABLE_TABLES); options.insert(Options::ENABLE_FOOTNOTES); options.insert(Options::ENABLE_STRIKETHROUGH); options.insert(Options::ENABLE_TASKLISTS); options.insert(Options::ENABLE_SMART_PUNCTUATION); options.insert(Options::ENABLE_HEADING_ATTRIBUTES); let mut text_kind = TextKind::Text; let syntax_set = SyntaxSet::load_defaults_newlines(); let theme_set = ThemeSet::load_defaults(); let theme = theme_set.themes.get("InspiredGitHub").unwrap(); let mut heading_ended: Option = None; let parser = Parser::new_ext(markdown, options).map(|event| match event { /* Parsing images considers `alt` attribute as inner `Text` event Therefore the `[alt]` is rendered in html as subtitle and the `[](url "title")` `title` is rendered as `alt` attribute */ Event::Start(Tag::Image { link_type: _, dest_url, title, id: _, }) => { if !dest_url.starts_with("/") { return Event::Html( formatdoc!( r#"{title}"# ) .into(), ); } let dev_only_img_path = Path::new("static/").join(dest_url.strip_prefix("/").unwrap_or(&dest_url)); let img_dimensions = image_dimensions(&dev_only_img_path).unwrap(); let (max_width, max_height) = get_max_resolution( img_dimensions, MAX_BLOG_IMAGE_RESOLUTION.0, MAX_BLOG_IMAGE_RESOLUTION.1, ); // Place image into the content with scaled reso to a boundary let picture_markup = generate_picture_markup( &dest_url, max_width, max_height, &title, None, ) .unwrap_or(formatdoc!( r#" {alt}"#, alt = title, src = dest_url, )); Event::Html( formatdoc!( r#"
{picture_markup}
"#, ) .into(), ) } Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) => { text_kind = TextKind::Code(lang.to_string()); Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) } Event::Text(text) => match &text_kind { TextKind::Code(lang) => { // TODO Check https://github.com/trishume/syntect/pull/535 for typescript support let lang = if ["ts".to_string(), "typescript".to_string()].contains(lang) { "javascript" } else { lang }; let syntax_reference = syntax_set .find_syntax_by_token(lang) .unwrap_or(syntax_set.find_syntax_plain_text()); let highlighted = highlighted_html_for_string(&text, &syntax_set, syntax_reference, theme) .unwrap(); Event::Html(highlighted.into()) } TextKind::Heading(provided_id) => { let heading_id = provided_id.clone().unwrap_or({ text.to_lowercase() .replace(|c: char| !c.is_alphanumeric(), "-") }); debug!("heading_id: {}", heading_id.clone()); match heading_ended { None => { error!("Heading should have set state"); panic!("Heading should have set state"); } Some(true) => Event::Html(text), Some(false) => { heading_ended = Some(true); Event::Html( formatdoc!( r##"id="{heading_id}"> {text}"## ) .into(), ) } } } _ => Event::Text(text), }, Event::Start(Tag::Heading { level, id, classes: _, attrs: _, }) => { let id_str = id.map(|id| id.to_string()); debug!("heading_start: {:?}, level: {}", &id_str, level); text_kind = TextKind::Heading(id_str); heading_ended = Some(false); Event::Html(format!("<{level} ").into()) } Event::Start(_) => event, Event::End(TagEnd::Image) => Event::Html("
".into()), Event::End(TagEnd::CodeBlock) => { text_kind = TextKind::Text; Event::End(TagEnd::CodeBlock) } Event::End(TagEnd::Heading(heading_level)) => { text_kind = TextKind::Text; heading_ended = None; Event::End(TagEnd::Heading(heading_level)) } _ => event, }); // Write to String buffer let mut html = String::new(); pulldown_cmark::html::push_html(&mut html, parser); Ok(html) }