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)),
)))),
));
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", "")
}
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);
}
}
}
}
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;
}
// 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}"
));
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 = 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 = &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("```") {
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)
}
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)
}