use std::collections::HashSet; pub fn render_markdown(input: &str) -> String { let html = tetratto_shared::markdown::render_markdown_dirty(&parse_text_color( &parse_highlight(&parse_link(&parse_image(&parse_image_size( &parse_underline(&parse_comment(&input.replace("[/]", "
"))), )))), )) .replace("$per", "%"); 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.replace("", ":temp_style"), allowed_attributes, ) .replace(":temp_style", "") } pub(crate) fn is_numeric(value: &str) -> bool { let mut is_numeric = false; for char in value.chars() { is_numeric = char.is_numeric(); } is_numeric } pub(crate) fn slice(x: &str, range: core::ops::RangeFrom) -> String { (&x.chars().collect::>()[range]) .iter() .collect::() } 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 (i, char) in line.chars().enumerate() { 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}" )); color_buffer.clear(); buffer.clear(); // ... in_main_buffer = false; close_1 = false; continue; } // start // scan ahead let ahead = slice(line, i..); if !ahead.contains("%%") { // no closing sequence, we're done buffer.push(char); continue; } // 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); } } } } fn parse_underline_line(output: &mut String, buffer: &mut String, line: &str) { let mut open_1 = false; let mut is_open = false; let mut close_1 = false; for char in line.chars() { if open_1 && char != '~' { is_open = false; open_1 = false; buffer.push('!'); } if close_1 && char != '!' { is_open = false; close_1 = false; buffer.push('~'); } match char { '~' => { if open_1 { open_1 = false; is_open = true; } else if is_open { // open close close_1 = true; } } '!' => { if close_1 { // close let mut s: Vec<&str> = buffer.split(";").collect(); let text = s.pop().unwrap_or(&"").trim(); let mut style = String::new(); for (i, mut x) in s.iter().enumerate() { if i == 0 { // color if x == &"default" { x = &"currentColor"; } style.push_str(&format!("text-decoration-color: {x};")); } else if i == 1 { // style if x == &"default" { x = &"solid"; } style.push_str(&format!("text-decoration-style: {x};")); } else if i == 2 { // line if x == &"default" { x = &"underline"; } style.push_str(&format!("text-decoration-line: {x};")); } else if i == 3 { // thickness if x == &"default" { x = &"1px"; } style.push_str(&format!("text-decoration-thickness: {x}px;")); } } // defaults if s.get(1).is_none() { style.push_str(&format!("text-decoration-style: solid;")); } if s.get(2).is_none() { style.push_str(&format!("text-decoration-line: underline;")); } if s.get(3).is_none() { style.push_str(&format!("text-decoration-thickness: 1px;")); } // ... output.push_str(&format!("{text}")); buffer.clear(); open_1 = false; is_open = false; close_1 = false; continue; } else if is_open { buffer.push(char); continue; } // open open_1 = true; // flush buffer output.push_str(&buffer); buffer.clear(); } _ => buffer.push(char), } } } fn parse_comment_line(output: &mut String, _: &mut String, line: &str) { if line.contains("]:") && line.starts_with("[") { return; } if line == "[..]" { output.push_str(" "); return; } output.push_str(line); } fn parse_image_size_line(output: &mut String, buffer: &mut String, line: &str) { let mut image_possible = false; let mut in_image = false; let mut in_size = false; let mut in_size_rhs = false; let mut size_lhs = String::new(); let mut size_rhs = String::new(); if !line.contains("{") { output.push_str(line); return; } for char in line.chars() { if image_possible && char != '[' { image_possible = false; output.push('!'); } match char { '[' => { if image_possible { in_image = true; image_possible = false; continue; } if in_image { buffer.push(char); } else { output.push(char); } } '{' => { if in_image { in_size = true; continue; } if in_image { buffer.push(char); } else { output.push(char); } } ':' => { if in_size { in_size_rhs = true; continue; } if in_image { buffer.push(char); } else { output.push(char); } } '}' => { if in_size && in_size_rhs { // end output.push_str(&format!( "![{buffer}", if is_numeric(&size_lhs) { format!("{size_lhs}px") } else { size_lhs }, if is_numeric(&size_rhs) { format!("{size_rhs}px") } else { size_rhs }, if buffer.ends_with("#left)") { "left" } else if buffer.ends_with("#right)") { "right" } else { "unset" } )); size_lhs = String::new(); size_rhs = String::new(); in_image = false; in_size = false; in_size_rhs = false; image_possible = false; buffer.clear(); continue; } if in_image { buffer.push(char); } else { output.push(char); } } '!' => { // flush buffer output.push_str(&buffer); buffer.clear(); // ... image_possible = true } _ => { if in_image { if in_size { if in_size_rhs { size_rhs.push(char); } else { size_lhs.push(char); } } else { buffer.push(char); } } else { output.push(char) } } } } } fn parse_image_line(output: &mut String, buffer: &mut String, line: &str) { let mut image_possible = false; let mut in_image = false; let mut in_alt = false; let mut in_src = false; let mut alt = String::new(); for char in line.chars() { if image_possible && char != '[' { image_possible = false; output.push('!'); } match char { '[' => { if image_possible { in_image = true; image_possible = false; in_alt = true; continue; } if in_image { buffer.push(char); } else { output.push(char); } } ']' => { if in_alt { in_alt = false; in_src = true; continue; } output.push(char); } '(' => { if in_src { continue; } if in_image { buffer.push(char); } else { output.push(char); } } ')' => { if in_image { // end output.push_str(&format!( "\"{alt}\"", buffer.replace(" ", "$per20"), if buffer.ends_with("#left") { "left" } else if buffer.ends_with("#right") { "right" } else { "unset" } )); alt = String::new(); in_alt = false; in_src = false; in_image = false; image_possible = false; buffer.clear(); continue; } output.push(char); } '!' => { // flush buffer output.push_str(&buffer); buffer.clear(); // ... image_possible = true; } _ => { if in_image { if in_alt { alt.push(char) } else { buffer.push(char); } } else { output.push(char) } } } } } fn parse_link_line(output: &mut String, buffer: &mut String, line: &str) { let mut in_link = false; let mut in_text = false; let mut in_src = false; let mut text = String::new(); for (i, char) in line.chars().enumerate() { match char { '[' => { // flush buffer output.push_str(&buffer); buffer.clear(); // scan for closing, otherwise quit let haystack = slice(line, i..); if !haystack.contains("]") { output.push('['); continue; } // ... in_link = true; in_text = true; } ']' => { if in_text { in_text = false; in_src = true; continue; } output.push(char); } '(' => { if in_src { continue; } if in_link { buffer.push(char); } else { output.push(char); } } ')' => { if in_link { // end output.push_str(&format!( "{text}" )); text = String::new(); in_text = false; in_src = false; in_link = false; buffer.clear(); continue; } output.push(char); } _ => { if in_link { if in_text { text.push(char) } else { buffer.push(char); } } else { output.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("```") | (line == "") { 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 }}; ($body:ident, $input:ident, ..) => {{ let mut in_pre_block = false; let mut output = String::new(); let mut buffer = String::new(); let mut proc_str = String::new(); for line in $input.split("\n") { if line.starts_with("```") | (line == "") { in_pre_block = !in_pre_block; output.push_str(&format!("{line}\n")); continue; } if in_pre_block { output.push_str(&format!("{line}\n")); continue; } proc_str += &(line.to_string() + "\n"); } $body(&mut output, &mut buffer, &proc_str); 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, ..) } pub fn parse_underline(input: &str) -> String { parser_ignores_pre!(parse_underline_line, input, ..) } pub fn parse_comment(input: &str) -> String { parser_ignores_pre!(parse_comment_line, input) } pub fn parse_image_size(input: &str) -> String { parser_ignores_pre!(parse_image_size_line, input) } pub fn parse_image(input: &str) -> String { parser_ignores_pre!(parse_image_line, input) } pub fn parse_link(input: &str) -> String { parser_ignores_pre!(parse_link_line, input) }