From 97ffcb48ecbd3e586beb0057454c99b2f722d351 Mon Sep 17 00:00:00 2001 From: trisua Date: Wed, 20 Aug 2025 13:52:41 -0400 Subject: [PATCH] fix: minimize markdown renderer --- src/markdown.rs | 1028 +---------------------------------------------- 1 file changed, 2 insertions(+), 1026 deletions(-) diff --git a/src/markdown.rs b/src/markdown.rs index 0a48004..44fc157 100644 --- a/src/markdown.rs +++ b/src/markdown.rs @@ -1,14 +1,7 @@ use std::collections::HashSet; pub fn render_markdown(input: &str) -> String { - let html = tetratto_shared::markdown::render_markdown_dirty(&parse_page(&parse_details( - &parse_text_color(&parse_highlight(&parse_link(&parse_image( - &parse_image_size(&parse_toc(&parse_underline(&parse_markdown_element( - &parse_comment(&input.replace("[/]", "
")), - )))), - )))), - ))) - .replace("$per", "%"); + let html = tetratto_shared::markdown::render_markdown_dirty(input); let mut allowed_attributes = HashSet::new(); allowed_attributes.insert("id"); @@ -20,1023 +13,6 @@ pub fn render_markdown(input: &str) -> String { allowed_attributes.insert("align"); allowed_attributes.insert("src"); allowed_attributes.insert("style"); - allowed_attributes.insert("controls"); - allowed_attributes.insert("autoplay"); - allowed_attributes.insert("loop"); - tetratto_shared::markdown::clean_html( - html.replace("", ":temp_style") - .replace("", ":temp_audio"), - allowed_attributes, - ) - .replace(":temp_style", "") - .replace(":temp_audio:temp_audio", "") -} - -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; - } - - if open_1 && char != '=' { - buffer.push('='); - open_1 = false; - is_open = 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; - - if char == '[' { - // image - buffer.push('!'); - } else { - buffer.push_str("!"); - } - } - - 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, $id:literal, ..) => {{ - let mut in_pre_block = false; - let mut output = String::new(); - let mut buffer = String::new(); - let mut proc_str = String::new(); - let mut pre_blocks = Vec::new(); - let mut pre_idx = 0; - - for line in $input.split("\n") { - if line.starts_with("```") { - in_pre_block = !in_pre_block; - - pre_idx += 1; - pre_blocks.push(String::new()); - pre_blocks[pre_idx - 1] += &(line.to_string() + "\n"); - - proc_str += &format!("$pre:{}.{pre_idx}\n", $id); - continue; - } - - if in_pre_block { - pre_blocks[pre_idx - 1] += &(line.to_string() + "\n"); - continue; - } - - proc_str += &(line.to_string() + "\n"); - } - - $body(&mut output, &mut buffer, &proc_str); - output.push_str(&format!("{buffer}\n")); - buffer.clear(); - - for (mut i, block) in pre_blocks.iter().enumerate() { - i += 1; - - if block == "```\n" { - output = output.replacen(&format!("$pre:{}.{i}", $id), "", 1); - continue; - } - - output = output.replacen(&format!("$pre:{}.{i}", $id), &format!("{block}```\n"), 1); - } - - output - }}; -} - -pub fn parse_text_color(input: &str) -> String { - parser_ignores_pre!(parse_text_color_line, input, 0, ..) -} - -pub fn parse_highlight(input: &str) -> String { - parser_ignores_pre!(parse_highlight_line, input, 1, ..) -} - -pub fn parse_underline(input: &str) -> String { - parser_ignores_pre!(parse_underline_line, input, 2, ..) -} - -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) -} - -/// Match page definitions. -/// -/// Each page is denoted with two at symbols, followed by the name of the page. -/// The page can also have an optional second argument (separated by a semicolon) -/// which accepts the "visible" value; marking the page as visible by default. -/// -/// To close a page (after you're done with the page's content), just put two -/// at symbols with nothing else on the line. -/// -/// You're able to put content AFTER the page closing line. This allows you to have -/// persistant content which is shared between every page. Only content within pages -/// is hidden when navigating to another page. This means everything in the entry -/// that isn't part of a page will remian throughout navigations. -/// -/// # Example -/// ```md -/// @@ home; visible -/// this is the homepage which is shown by default! -/// @@ -/// -/// @@ about -/// this is the about page which is NOT shown by default! a link with an href of "#/about" will open this page -/// @@ -/// ``` -pub fn parse_page(input: &str) -> String { - let mut output = String::new(); - let mut buffer = String::new(); - let mut page_id = String::new(); - - let mut start_shown = false; - let mut in_page = false; - let mut in_pre = false; - - for line in input.split("\n") { - if line.starts_with("```") || line.starts_with("") { - in_pre = !in_pre; - - if in_page { - buffer.push_str(&format!("{line}\n")); - } else { - output.push_str(&format!("{line}\n")); - } - - continue; - } - - if in_pre { - if in_page { - buffer.push_str(&format!("{line}\n")); - } else { - output.push_str(&format!("{line}\n")); - } - - continue; - } - - // not in pre - if line == "@@" { - // ending block - if in_page { - output.push_str(&format!( - "
\n{}\n
", - if !start_shown { "hidden " } else { "" }, - render_markdown(&buffer) // recurse to render markdown since the renderer is ignoring the div content :/ - )); - - start_shown = false; - in_page = false; - - buffer.clear(); - continue; - } - } else if line.starts_with("@@") { - if !in_page { - in_page = true; - - let x = line.replace("@@", "").trim().to_string(); - let id_parts: Vec<&str> = x.split(";").map(|x| x.trim()).collect(); - page_id = id_parts[0].to_string(); - - if let Some(x) = id_parts.get(1) { - if *x == "visible" { - start_shown = true; - } - } - - continue; - } - } - - // otherwise - if in_page { - buffer.push_str(&format!("{line}\n")); - } else { - output.push_str(&format!("{line}\n")); - } - } - - output -} - -/// Parse the markdown syntax for the expandable `
` element. -/// -/// Similar to the [`parse_page`] page definitions, details elements are denoted -/// with two ampersand symbols. The opening line should look like `&& [summary]`. -/// -/// The block is closed with a line of exactly two ampersand symbols. -/// -/// # Example -/// ```md -/// && other summary -/// this element starts closed, but can be expanded -/// && -/// ``` -pub fn parse_details(input: &str) -> String { - let mut output = String::new(); - let mut buffer = String::new(); - let mut summary = String::new(); - - let mut in_details = false; - let mut in_pre = false; - - for line in input.split("\n") { - if line.starts_with("```") || line.starts_with("") { - in_pre = !in_pre; - - if in_details { - buffer.push_str(&format!("{line}\n")); - } else { - output.push_str(&format!("{line}\n")); - } - - continue; - } - - if in_pre { - if in_details { - buffer.push_str(&format!("{line}\n")); - } else { - output.push_str(&format!("{line}\n")); - } - - continue; - } - - // not in pre - if line == "&&" { - // ending block - if in_details { - output.push_str(&format!( - "
{summary}
{}
", - render_markdown(&buffer), - )); - - in_details = false; - buffer.clear(); - continue; - } - } else if line.starts_with("&&") { - if !in_details { - in_details = true; - summary = line.replace("&&", "").trim().to_string(); - continue; - } - } - - // otherwise - if in_details { - buffer.push_str(&format!("{line}\n")); - } else { - output.push_str(&format!("{line}\n")); - } - } - - output -} - -fn underscore_chars(mut x: String, chars: &[&str]) -> String { - for y in chars { - x = x.replace(y, "_"); - } - - x -} - -/// Get the list of headers needed for [`parse_toc`]. -pub fn get_toc_list(input: &str) -> (String, String) { - let mut output = String::new(); - let mut toc = String::new(); - let mut in_pre = false; - let mut hc_offset: Option = None; - - for line in input.split("\n") { - if line.starts_with("```") || line.starts_with("") { - in_pre = !in_pre; - output.push_str(&format!("{line}\n")); - continue; - } - - if in_pre { - output.push_str(&format!("{line}\n")); - continue; - } - - // not in pre - if line.starts_with("#") { - // get heading count - let mut hc = 0; - let real_hc; - - for x in line.chars() { - if x != '#' { - break; - } - - hc += 1; - } - - real_hc = hc.clone(); - if hc_offset.is_none() { - if hc > 1 { - // offset this count to 1 so the list renders properly - hc_offset = Some(hc - 1); - hc = 1; - } else { - hc_offset = Some(0); - } - } else if let Some(offset) = hc_offset { - hc -= offset; - } - - // add heading with id - let x = line.replacen(&"#".repeat(real_hc), "", 1); - let htext = x.trim(); - - let id = underscore_chars( - htext.to_lowercase(), - &[" ", "(", ")", "[", "]", "{", "}", ":", "?", "#", "&"], - ); - - output.push_str(&format!( - "{}\n\n", - render_markdown(&htext) - )); - - // add heading to toc - toc += &format!("{}- {htext}\n", " ".repeat(hc)); - - // ... - continue; - } - - // otherwise - output.push_str(&format!("{line}\n")); - } - - (toc, output) -} - -/// Parse the `[toc]` table-of-contents syntax. -pub fn parse_toc(input: &str) -> String { - let (toc_list, new_input) = get_toc_list(input); - - let mut output = String::new(); - let mut in_pre = false; - - for line in new_input.split("\n") { - if line.starts_with("```") || line.starts_with("") { - in_pre = !in_pre; - output.push_str(&format!("{line}\n")); - continue; - } - - if in_pre { - output.push_str(&format!("{line}\n")); - continue; - } - - // not in pre - if line.len() == 5 && line.to_lowercase() == "[toc]" { - // add toc - output.push_str(&format!("\n{toc_list}")); - continue; - } - - // otherwise - output.push_str(&format!("{line}\n")); - } - - output -} - -/// Handle the `` HTML element. -fn parse_markdown_element_line(output: &mut String, buffer: &mut String, line: &str) { - let mut in_markdown = false; - - for char in line.chars() { - if buffer.ends_with("") { - in_markdown = true; - output.push_str(&buffer.replace("", "")); - buffer.clear(); - } else if in_markdown && buffer.ends_with("") { - in_markdown = false; - output.push_str(&render_markdown(&buffer.replace("", ""))); - buffer.clear(); - } - - buffer.push(char); - } -} - -pub fn parse_markdown_element(input: &str) -> String { - parser_ignores_pre!(parse_markdown_element_line, input) + tetratto_shared::markdown::clean_html(html, allowed_attributes) }