608 lines
17 KiB
Rust
608 lines
17 KiB
Rust
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("<style>", "<span>:temp_style")
|
|
.replace("</style>", "</span>:temp_style"),
|
|
allowed_attributes,
|
|
)
|
|
.replace("<span>:temp_style", "<style>")
|
|
.replace("</span>:temp_style", "</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!(
|
|
"<span style=\"color: {color_buffer}\">{buffer}</span>\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!("<mark>{buffer}</mark>\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!("<span style=\"{style}\">{text}</span>"));
|
|
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!(
|
|
"<span style=\"width: {size_lhs}; height: {size_rhs}\" class=\"img_sizer\">![{buffer}</span>"
|
|
));
|
|
|
|
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!(
|
|
"<img loading=\"lazy\" alt=\"{alt}\" src=\"{buffer}\" />"
|
|
));
|
|
|
|
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!(
|
|
"<a href=\"{buffer}\" rel=\"noopener noreferrer\">{text}</a>"
|
|
));
|
|
|
|
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)
|
|
}
|