use crate::misc::date::Date; use comrak::nodes::{AstNode, NodeValue}; use comrak::{ format_html, parse_document, Arena, ComrakExtensionOptions, ComrakOptions, ComrakParseOptions, ComrakRenderOptions, ListStyleType, }; use ramhorns::Content; use serde::Deserialize; use std::fs; #[derive(Default, Deserialize, Content)] pub struct FileMetadata { pub title: Option, pub link: Option, pub date: Option, } #[derive(Content)] pub struct Metadata { pub info: FileMetadata, pub math: bool, pub mermaid: bool, pub syntax_highlight: bool, } #[derive(Content)] pub struct File { pub metadata: Metadata, pub content: String, } /// Options used for parser and compiler MD --> HTML pub fn get_options() -> ComrakOptions { ComrakOptions { extension: ComrakExtensionOptions { strikethrough: true, tagfilter: true, table: true, autolink: true, tasklist: true, superscript: true, header_ids: Some(String::new()), footnotes: false, // true? description_lists: true, front_matter_delimiter: Some("---".to_owned()), }, parse: ComrakParseOptions { smart: false, // true one day? default_info_string: None, relaxed_tasklist_matching: true, }, render: ComrakRenderOptions { hardbreaks: false, // could be true? change by metadata could be good for compatibility github_pre_lang: false, full_info_string: true, width: 0, // 0 mean disabled? unsafe_: false, // could be true? change by metadata could be good for compatibility escape: false, // may change in the future? list_style: ListStyleType::Dash, sourcepos: false, }, } } pub fn read(raw_text: &str) -> File { let arena = Arena::new(); let options = get_options(); let root = parse_document(&arena, raw_text, &options); // Find metadata let metadata = get_metadata(root); // Convert to HTML let mut html = vec![]; format_html(root, &options, &mut html).unwrap(); let mermaid_name = "mermaid"; File { metadata: Metadata { info: metadata, mermaid: check_mermaid(root, mermaid_name.to_owned()), syntax_highlight: check_code(root, &[mermaid_name.to_owned()]), }, content: String::from_utf8(html).unwrap(), } } /// Read markdown file pub fn read_file(filename: &str) -> Option { match fs::read_to_string(filename) { Ok(text) => Some(read(&text)), _ => None, } } /// Fetch metadata from AST pub fn get_metadata<'a>(root: &'a AstNode<'a>) -> FileMetadata { match root .children() .find_map(|node| match &node.data.borrow().value { NodeValue::FrontMatter(text) => { // '-' correspond to `front_matter_delimiter` serde_yaml::from_str(text.trim_matches(&['-', '\n'] as &[_])).unwrap_or_default() } _ => None, }) { Some(data) => data, None => FileMetadata::default(), } } /// Recursively check whether mermaid diagrams are in the AST fn check_mermaid<'a>(root: &'a AstNode<'a>, mermaid_str: String) -> bool { root.children().any(|node| match &node.data.borrow().value { // Check if code of block define a mermaid diagram NodeValue::CodeBlock(code_block) => code_block.info == mermaid_str, _ => false, }) } /// Recursively check if code is in the AST fn check_code<'a>(root: &'a AstNode<'a>, blacklist: &[String]) -> bool { root.children().any(|node| match &node.data.borrow().value { // Detect code in paragraph /* NodeValue::Paragraph => match &node.children().next() { Some(child) => matches!(child.data.borrow().value, NodeValue::Code(_)), None => false, }, */ // Detect blocks of code where the lang isn't in the blacklist NodeValue::CodeBlock(code_block) => !blacklist.contains(&code_block.info), _ => false, }) }