use std::collections::HashSet; pub fn render_markdown(input: &str) -> String { let html = tetratto_shared::markdown::render_markdown_dirty(&parse_text_color( &parse_highlight(input), )); let mut allowed_attributes = HashSet::new(); allowed_attributes.insert("id"); allowed_attributes.insert("class"); allowed_attributes.insert("ref"); allowed_attributes.insert("aria-label"); allowed_attributes.insert("lang"); allowed_attributes.insert("title"); allowed_attributes.insert("align"); allowed_attributes.insert("src"); allowed_attributes.insert("style"); tetratto_shared::markdown::clean_html(html, allowed_attributes) } fn parse_text_color_line(output: &mut String, buffer: &mut String, line: &str) { let mut in_color_buffer = false; let mut in_main_buffer = false; let mut color_buffer = String::new(); let mut close_1 = false; for char in line.chars() { if close_1 && char != '%' { // we expected to see another percentage to close the main buffer, // not getting that means this wasn't meant to be a color buffer.push('%'); in_main_buffer = false; close_1 = false; } match char { '%' => { if in_color_buffer { in_color_buffer = false; in_main_buffer = true; continue; } if in_main_buffer { // ending if !close_1 { close_1 = true; continue; } // by this point, we have: ! // %color_buffer%main_buffer%% output.push_str(&format!( "{buffer}\n" )); color_buffer.clear(); buffer.clear(); // ... in_main_buffer = false; close_1 = false; continue; } // start // flush buffer output.push_str(&buffer); buffer.clear(); // toggle open in_color_buffer = true; } ' ' => { if in_color_buffer == true { buffer.push_str(&color_buffer); color_buffer.clear(); } buffer.push(char); } _ => { if in_color_buffer { color_buffer.push(char) } else { buffer.push(char) } } } } } fn parse_highlight_line(output: &mut String, buffer: &mut String, line: &str) { let mut open_1 = false; let mut open_2 = false; let mut close_1 = false; let mut is_open = false; for char in line.chars() { if close_1 && char != '=' { buffer.push('='); close_1 = false; } match char { '=' => { if !is_open { // flush buffer output.push_str(&buffer); buffer.clear(); // toggle open open_1 = true; is_open = true; } else { if open_1 { // this is the second open we've recieved open_2 = true; open_1 = false; continue; } if close_1 { // this is the second close we've received output.push_str(&format!("{buffer}\n")); buffer.clear(); open_1 = false; open_2 = false; close_1 = false; is_open = false; continue; } close_1 = true; } } _ => { if open_1 { open_1 = false; buffer.push('='); } if open_2 && is_open { open_2 = false; } buffer.push(char); } } } } /// Helper macro to quickly allow parsers to ignore fenced code blocks. macro_rules! parser_ignores_pre { ($body:ident, $input:ident) => {{ let mut in_pre_block = false; let mut output = String::new(); let mut buffer = String::new(); for line in $input.split("\n") { if line.starts_with("```") { in_pre_block = !in_pre_block; output.push_str(&format!("{line}\n")); continue; } if in_pre_block { output.push_str(&format!("{line}\n")); continue; } $body(&mut output, &mut buffer, line); output.push_str(&format!("{buffer}\n")); buffer.clear(); } output }}; } pub fn parse_text_color(input: &str) -> String { parser_ignores_pre!(parse_text_color_line, input) } pub fn parse_highlight(input: &str) -> String { parser_ignores_pre!(parse_highlight_line, input) }