[add] new markdown renderer

[chore] bump version (v0.8.2 -> v0.9.0)
This commit is contained in:
hkau 2024-02-25 18:16:21 -05:00
parent ddaf4a0eb9
commit be99332a20
8 changed files with 422 additions and 535 deletions

View file

@ -3,7 +3,7 @@ name = "bundlrs"
authors = ["hkau"]
license = "MIT"
version = "0.8.2"
version = "0.9.0"
edition = "2021"
rust-version = "1.75"
@ -19,12 +19,14 @@ default = ["sqlite"]
[dependencies]
actix-files = "0.6.5"
actix-web = "4.4.1"
comrak = "0.21.0"
dotenv = "0.15.0"
either = "1.9.0"
env_logger = "0.11.2"
hex_fmt = "0.3.0"
idna = "0.5.0"
pulldown-cmark = "0.9.3"
pest = "2.7.7"
pest_derive = "2.7.7"
regex = "1.10.2"
serde = "1.0.195"
serde_json = "1.0.111"

View file

@ -51,7 +51,7 @@ struct MetadataInfo {
pub async fn render_request(body: web::Json<RenderInfo>) -> impl Responder {
return HttpResponse::Ok()
.append_header(("Content-Type", "text/html"))
.body(markdown::parse_markdown(&body.text));
.body(markdown::render::parse_markdown(&body.text));
}
#[post("/api/ssm")]
@ -153,7 +153,7 @@ pub async fn create_request(
id: String::new(), // reassigned anyways, this doesn't matter
edit_password: edit_password.to_string(),
content: content.clone(),
content_html: crate::markdown::parse_markdown(&content), // go ahead and render the content
content_html: crate::markdown::render::parse_markdown(&content), // go ahead and render the content
pub_date: utility::unix_epoch_timestamp(),
edit_date: utility::unix_epoch_timestamp(),
group_name: g_name_for_real.to_string(),

View file

@ -1105,7 +1105,7 @@ impl BundlesDB {
let c = &self.db.client;
let res = sqlx::query(query)
.bind::<&String>(&content)
.bind::<&String>(&crate::markdown::parse_markdown(&content))
.bind::<&String>(&crate::markdown::render::parse_markdown(&content))
.bind::<&String>(&edit_password_hash)
.bind::<&String>(&custom_url)
.bind::<&String>(&utility::unix_epoch_timestamp().to_string()) // update edit_date

View file

@ -1,530 +0,0 @@
use crate::ssm;
use regex::RegexBuilder;
#[allow(dead_code)]
struct Heading<'l> {
pub text: &'l str,
pub level: usize,
pub id: String,
}
/// Parse raw Markdown input into HTML
///
/// # Arguments:
/// * `input` - `String` containing the Markdown input to be parsed
pub fn parse_markdown(input: &String) -> String {
let mut out: String = input.to_owned();
// escape < and >
out = regex_replace(&out, "<", "&lt;");
out = regex_replace(&out, ">", "&gt;");
// unescape arrow alignment
out = regex_replace(&out, "-&gt;&gt;", "->>");
out = regex_replace(&out, "&lt;&lt;-", "<<-");
out = regex_replace(&out, "-&gt;", "->");
out = regex_replace(&out, "&lt;-", "<-");
// allowed elements
let allowed_elements: Vec<&str> = Vec::from([
"hue", "sat", "lit", "theme", "comment", "p", "span", "style",
]);
for element in allowed_elements {
out = regex_replace(
&out,
&format!("&lt;{}&gt;", element),
&format!("<{}>", element),
);
out = regex_replace(
&out,
&format!("&lt;/{}&gt;", element),
&format!("</{}>", element),
);
}
// HTML escapes
out = regex_replace(&out, "(&!)(.*?);", "&$2;");
// backslash escapes
out = out.replace(r"\*", "&ast;");
// backslash line continuation
out = out.replace("\\\n", "");
// fenced code blocks
let mut fenced_code_block_count: i32 = 0;
let fenced_code_block_regex = RegexBuilder::new("^(`{3})(.*?)\\n(.*?)(`{3})$")
.multi_line(true)
.dot_matches_new_line(true)
.build()
.unwrap();
for capture in fenced_code_block_regex.captures_iter(&out.clone()) {
let lang = capture.get(2).unwrap().as_str();
let mut content = capture.get(3).unwrap().as_str().to_string();
fenced_code_block_count += 1;
// run replacements
content = content.replace("*", "&!temp-ast;");
content = content.replace("`", "&!temp-back;");
content = content.replace("\\n", "&nbsp;1;\\n");
content = content.replace("#", "&#35;");
content = content.replace("(", "&lpar;");
// build line numbers
let mut line_numbers: String = String::new();
let mut _current_ln: i32 = 0;
for line in content.split("\n") {
if line.is_empty() {
continue;
};
_current_ln += 1;
line_numbers = format!(
"{}<a class=\"line-number\" href=\"#B{}L{}\" id=\"B{}L{}\">{}</a>\n",
line_numbers,
fenced_code_block_count,
_current_ln,
fenced_code_block_count,
_current_ln,
_current_ln
);
}
// replace
out = out.replace( capture.get(0).unwrap().as_str(), &format!("<pre class=\"flex\" style=\"position: relative;\">
<div class=\"line-numbers code\">{line_numbers}</div>
<code class=\"language-${lang}\" id=\"B{fenced_code_block_count}C\" style=\"display: block;\">{content}</code>
<button
onclick=\"window.navigator.clipboard.writeText(document.getElementById('B{fenced_code_block_count}C').innerText);\"
class=\"secondary copy-button\"
title=\"Copy Code\"
>
<svg
xmlns=\"http://www.w3.org/2000/svg\"
width=\"18\"
height=\"18\"
viewBox=\"0 0 24 24\"
fill=\"none\"
stroke=\"currentColor\"
stroke-width=\"2\"
stroke-linecap=\"round\"
stroke-linejoin=\"round\"
class=\"lucide lucide-clipboard-copy\"
>
<rect
width=\"8\"
height=\"4\"
x=\"8\"
y=\"2\"
rx=\"1\"
ry=\"1\"
/>
<path d=\"M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2\" />
<path d=\"M16 4h2a2 2 0 0 1 2 2v4\" />
<path d=\"M21 14H11\" />
<path d=\"m15 10-4 4 4 4\" />
</svg>
</button>
</pre>"));
}
// inline code block
out = regex_replace(&out, "(`{1})(.*?)(`{1})", "<code>$2</code>");
// headings
let table_of_contents: &mut Vec<Heading> = &mut Vec::new();
let heading_regex = RegexBuilder::new("^(\\#+)\\s(.*?)$")
.multi_line(true)
.build()
.unwrap();
for capture in heading_regex.captures_iter(&out.clone()) {
let heading_type = capture.get(1).unwrap().as_str().len();
let content = capture.get(2).unwrap().as_str();
// get suffix
// (get all headings with the same text, suffix is the number of those)
// (helps prevent duplicate ids)
let same_headings = table_of_contents.iter().filter(|h| h.text == content);
let count = same_headings.count() as i32;
let suffix = if &count == &0 {
"".to_string()
} else {
format!("-{}", count)
};
// add to TOC
let heading_id = regex_replace(
&format!("{content}{suffix}").to_lowercase(),
"[^A-Za-z0-9-]",
"",
);
table_of_contents.push(Heading {
text: content,
level: heading_type,
id: heading_id.clone(),
});
// return
out = out.replace(
capture.get(0).unwrap().as_str(),
format!("<h{heading_type} id=\"{heading_id}\">{content}</h{heading_type}>\n").as_str(),
)
}
// remove frontmatter
regex_replace_exp(
&out,
RegexBuilder::new("^(\\-{3})F\\n(?<CONTENT>.*?)\\n(\\-{3})F$")
.multi_line(true)
.dot_matches_new_line(true),
"",
);
// horizontal rule
out = regex_replace(&out, "^\\*{3,}", "\n<hr />\n");
out = regex_replace(&out, "^\\-{3,}", "\n<hr />\n");
out = regex_replace(&out, "^\\_{3,}", "\n<hr />\n");
// special custom element syntax (rs)
let custom_element_regex = RegexBuilder::new("(e\\#)(?<NAME>.*?)\\s(?<ATRS>.*?)\\#")
.multi_line(true)
.build()
.unwrap();
for capture in custom_element_regex.captures_iter(&out.clone()) {
let name = capture.name("NAME").unwrap().as_str();
let atrs = capture.name("ATRS").unwrap().as_str().replace("$", "#");
let mut atrs_split: Vec<String> = atrs.split("+").map(|s| s.to_string()).collect();
// make sure everything exists (before we try to call .unwrap on them!)
if atrs_split.get(0).is_none() {
atrs_split.insert(0, String::new())
}
if atrs_split.get(1).is_none() {
atrs_split.insert(1, String::new())
}
if atrs_split.get(2).is_none() {
atrs_split.insert(2, String::new())
}
if atrs_split.get(3).is_none() {
atrs_split.insert(3, String::new())
}
if atrs_split.get(4).is_none() {
atrs_split.insert(4, String::new())
}
// possibilities
let possible_error_block =
&"\n!!! error parsing error: invalid element class in element block".to_string();
let possible_theme_block = &format!("<theme>{}</theme>", atrs_split.get(0).unwrap());
let possible_hsl_block = &format!(
"<{}>{}</{}>",
atrs_split.get(0).unwrap(),
atrs_split.get(1).unwrap(),
atrs_split.get(0).unwrap()
);
let possible_html_block = &format!("<{}>", atrs_split.get(0).unwrap());
let possible_chtml_block = &format!("</{}>", atrs_split.get(0).unwrap());
let possible_class_block = &format!("<span class=\"{}\">", atrs.replace("+", " "));
let possible_id_block = &format!("<span id=\"{}\">", atrs_split.get(0).unwrap());
let possible_close_block = &format!("</span>");
let possible_animation_block = &format!(
"<span role=\"animation\" style=\"
animation:{} {} ease-in-out {} forwards running;
display: block;\"
>{}",
// name
atrs_split.get(0).unwrap(),
// duration
if atrs_split.get(1).is_some() {
atrs_split.get(1).unwrap()
} else {
"1s"
},
// delay
if atrs_split.get(2).is_some() {
atrs_split.get(2).unwrap()
} else {
"0s"
},
// iterations
// infinite works here too
if atrs_split.get(3).is_some() {
atrs_split.get(3).unwrap()
} else {
"1"
}
);
// build result
let result = match name {
// theming
"theme" => &possible_theme_block,
"hsl" => &possible_hsl_block,
"animation" => &possible_animation_block,
// html
"html" => possible_html_block,
"chtml" => possible_chtml_block,
"class" => possible_class_block,
"id" => possible_id_block,
"close" => possible_close_block,
// (error message by default)
&_ => possible_error_block,
};
// replace
out = out.replace(capture.get(0).unwrap().as_str(), &result);
}
// ssm
// essentially just ssm::parse_ssm_blocks, maybe clean this up later?
let ssm_regex = RegexBuilder::new("(ssm\\#)(?<CONTENT>.*?)\\#")
.multi_line(true)
.dot_matches_new_line(true)
.build()
.unwrap();
for capture in ssm_regex.captures_iter(&out.clone()) {
let content = capture.name("CONTENT").unwrap().as_str().replace("$", "#");
// compile
let css = ssm::parse_ssm_program(content.to_string());
// replace
out = out.replace(
capture.get(0).unwrap().as_str(),
&format!("<style>{css}</style>"),
);
}
// text color (bundlrs style)
let color_regex = RegexBuilder::new("(c\\#)\\s*(?<COLOR>.*?)\\s*\\#\\s*(?<CONTENT>.*?)\\s*\\#")
.multi_line(true)
.dot_matches_new_line(true)
.build()
.unwrap();
for capture in color_regex.captures_iter(&out.clone()) {
let content = capture.name("CONTENT").unwrap().as_str();
let color = capture.name("COLOR").unwrap().as_str().replace("$", "#");
// replace
out = out.replace(
capture.get(0).unwrap().as_str(),
&format!("<span style=\"color: {color}\">{content}</span>"),
);
}
// text color thing
out = regex_replace_exp(
&out,
RegexBuilder::new(r"%(.*?)%\s*(.*?)\s*(%{2})")
.multi_line(true)
.dot_matches_new_line(true),
"<span style=\"color: $1;\" role=\"custom-color\">$2</span>",
);
// spoiler
out = regex_replace(
&out,
"(\\|\\|)\\s*(?<CONTENT>.*?)\\s*(\\|\\|)",
"<span role=\"spoiler\">$2</span>",
);
out = regex_replace(
&out,
"(\\!\\&gt;)\\s*(?<CONTENT>.*?)($|\\s\\s)",
"<span role=\"spoiler\">$2</span>",
);
// admonitions
out = regex_replace(
// title and content
&out,
"^(\\!{3})\\s(?<TYPE>.*?)\\s(?<TITLE>.+)\\n(?<CONTENT>.+)$",
"<div class=\"mdnote note-$2\">
<b class=\"mdnote-title\">$3</b>
<p>$4</p>
</div>\n",
);
out = regex_replace(
// title only
&out,
"^(\\!{3})\\s(?<TYPE>.*?)\\s(?<TITLE>.*?)$",
"<div class=\"mdnote note-$2\"><b class=\"mdnote-title\">$3</b></div>\n",
);
// highlight
out = regex_replace(
&out,
"(\\={2})(.*?)(\\={2})",
"<span class=\"highlight\">$2</span>",
);
// we have to do this ourselves because the next step would make it not work!
out = regex_replace(
&out,
"(\\*{3})(.*?)(\\*{3})",
"<strong><em>$2</em></strong>",
);
// manual bold/italics
out = regex_replace(&out, "(\\*{2})(.*?)(\\*{2})", "<strong>$2</strong>");
out = regex_replace(&out, "(\\*{1})(.*?)(\\*{1})", "<em>$2</em>");
// undo code replacements
out = out.replace("&!temp-ast;", "*");
out = out.replace("&!temp-back;", "`");
out = out.replace("&nbsp;1;\n", "&nbsp;\n");
// strikethrough
out = regex_replace(&out, "(\\~{2})(.*?)(\\~{2})", "<del>$2</del>");
// underline
out = regex_replace(
&out,
"(\\_{2})(.*?)(\\_{2})",
"<span style=\"text-decoration: underline;\" role=\"underline\">$2</span>",
);
// arrow alignment (flex)
let arrow_alignment_flex_regex = RegexBuilder::new("(\\->{2})(.*?)(\\->{2}|<{2}\\-)")
.multi_line(true)
.dot_matches_new_line(true)
.build()
.unwrap();
for capture in arrow_alignment_flex_regex.captures_iter(&out.clone()) {
let _match = capture.get(0).unwrap().as_str();
let content = capture.get(2).unwrap().as_str();
let align = if _match.ends_with(">") {
"right"
} else {
"center"
};
out = out.replace(
_match,
&format!("<rf style=\"justify-content: {align}\">{content}</rf>\n"),
);
}
// arrow alignment
let arrow_alignment_regex = RegexBuilder::new("(\\->{1})(.*?)(\\->{1}|<{1}\\-)")
.multi_line(true)
.dot_matches_new_line(true)
.build()
.unwrap();
for capture in arrow_alignment_regex.captures_iter(&out.clone()) {
let _match = capture.get(0).unwrap().as_str();
let content = capture.get(2).unwrap().as_str();
let align = if _match.ends_with(">") {
"right"
} else {
"center"
};
out = out.replace(
_match,
&format!("<r style=\"text-align: {align}\">{content}</r>\n"),
);
}
// image with sizing
let image_sizing_regex = RegexBuilder::new("(!)\\[(.*?)\\]\\((.*?)\\)\\:\\{(.*?)x(.*?)\\}")
.multi_line(true)
.build()
.unwrap();
for capture in image_sizing_regex.captures_iter(&out.clone()) {
let title = capture.get(2).unwrap().as_str();
let src = capture.get(3).unwrap().as_str();
let width = capture.get(4).unwrap().as_str();
let height = capture.get(5).unwrap().as_str();
let result = &format!("<img alt=\"{title}\" title=\"{title}\" src=\"{src}\" style=\"width: {width}px; height: {height}px;\" />");
out = out.replace(capture.get(0).unwrap().as_str(), result);
}
// normal image
out = regex_replace(
&out,
"(!)\\[(.*?)\\]\\((.*?)\\)",
"<img alt=\"$2\" title=\"$2\" src=\"$3\" />",
);
// anchor (attributes)
out = regex_replace(
&out,
"\\[(?<TEXT>.*?)\\]\\((?<URL>.*?)\\)\\:\\{(?<ATTRS>.+)\\}",
"<a href=\"$1\" $3>$1</a>",
);
// bath time
out = regex_replace(&out, "^(on)(.*)\\=(.*)\"$", "");
out = regex_replace(&out, "(href)\\=\"(javascript\\:)(.*)\"", "");
out = regex_replace(&out, "(<script.*>)(.*?)(<\\/script>)", "");
out = regex_replace(&out, "(<script.*>)", "");
out = regex_replace(&out, "(<link.*>)", "");
out = regex_replace(&out, "(<meta.*>)", "");
// return
let parser = pulldown_cmark::Parser::new(&out);
let mut html_out = String::new();
pulldown_cmark::html::push_html(&mut html_out, parser);
return html_out;
}
#[allow(dead_code)]
fn regex_replace(input: &str, pattern: &str, replace_with: &str) -> String {
return RegexBuilder::new(pattern)
.multi_line(true)
.build()
.unwrap()
.replace_all(input, replace_with)
.to_string();
}
#[allow(dead_code)]
fn regex_replace_exp(input: &str, pattern: &mut RegexBuilder, replace_with: &str) -> String {
return pattern
.build()
.unwrap()
.replace_all(input, replace_with)
.to_string();
}
#[allow(dead_code)]
fn regex_replace_one(input: &str, pattern: &str, replace_with: &str) -> String {
return RegexBuilder::new(pattern)
.build()
.unwrap()
.replace(input, replace_with)
.to_string();
}

29
src/markdown/argon.pest Normal file
View file

@ -0,0 +1,29 @@
// this grammar is ONLY for the bundlrs' custom templating and formatting language!
// for markdown formatting, please see [comrak](https://hrzn.ee/kivikakk/comrak)
// license available here: <https://code.stellular.org/SentryTwo/bundlrs/src/branch/master/LICENSE>
FILE = { SOI ~ (BLOCKS | ANY)* ~ EOI }
BLOCKS = _{ ELEMENTS }
ELEMENTS = _{ ("e#" | "<%") ~ (THEME | HSL | HTML | CHTML | CLASS | ID | CLOSE | ANIMATION) ~ ("#" | "%>") }
THEME = { "theme" ~ IDENTIFIER }
HSL = { "hsl" ~ IDENTIFIER ~ IDENTIFIER }
HTML = { "html" ~ IDENTIFIER+ }
CHTML = { "chtml" ~ IDENTIFIER }
CLASS = { "class" ~ IDENTIFIER+ }
ID = { "id" ~ IDENTIFIER }
CLOSE = { "close" ~ IDENTIFIER? }
ANIMATION = { "animation" ~ IDENTIFIER+ }
// ...
WHITESPACE = _{ " " | "\t" | "\r" | "\n" | "," }
COMMENT = _{ "//#" ~ (!NEWLINE ~ ANY)* }
IDENTIFIER = @{ (ASCII_ALPHANUMERIC | "_" | "-" | ":" | "\"" | "'" | "=" | "&" | ";" | "[" | "]" | "(" | ")" | "<" | ">")+ }
// DIGITS = @{ (ASCII_DIGIT | ("_" ~ ASCII_DIGIT))+ }
// INT = @{ "0" | (ASCII_NONZERO_DIGIT ~ DIGITS?) }
// NUM = @{ ("+" | "-")? ~ INT ~ ("." ~ DIGITS ~ EXP? | EXP)? }
// EXP = @{ ("E" | "e") ~ ("+" | "-")? ~ INT }
INNER = @{ (!("\"" | "\\" | "\u{0000}" | "\u{001F}") ~ ANY)* ~ (ESCAPE ~ INNER)? }
ESCAPE = @{ "\\" ~ ("b" | "t" | "n" | "f" | "r" | "\"" | "\\" | NEWLINE)? }

4
src/markdown/mod.rs Normal file
View file

@ -0,0 +1,4 @@
//! Markdown Renderer
pub mod parser;
pub mod render;

18
src/markdown/parser.rs Normal file
View file

@ -0,0 +1,18 @@
use pest::{iterators::Pair, Parser};
use pest_derive::Parser;
#[derive(Parser)]
#[grammar = "markdown/argon.pest"]
pub struct ArParser;
pub fn parse(input: &str) -> Pair<'_, Rule> {
let res = ArParser::parse(Rule::FILE, input);
if res.is_err() {
let err_unwrap = res.err().unwrap();
panic!("({})\n{}", err_unwrap.variant, err_unwrap);
}
// return
return res.unwrap().next().unwrap();
}

364
src/markdown/render.rs Normal file
View file

@ -0,0 +1,364 @@
use super::parser::Rule;
use comrak::{markdown_to_html, Options};
use pest::iterators::{Pair, Pairs};
use regex::RegexBuilder;
pub fn from_tree(tree: &Pairs<'_, Rule>, original_in: String) -> String {
let mut options = Options::default();
options.extension.table = true;
options.extension.superscript = true;
options.extension.strikethrough = true;
options.extension.autolink = true;
let mut out: String = markdown_to_html(&original_in, &options);
out = regex_replace(&out, "(&!)(.*?);", "&$2;");
// ...
for block in tree.clone().into_iter() {
let btype = block.as_rule();
let block_string = block.as_span().as_str().to_string();
let inner = block.into_inner().collect::<Vec<Pair<'_, Rule>>>();
// e#theme (identifier)#
if btype == Rule::THEME {
let theme = inner.get(0).unwrap();
out = out.replace(
&format!("e#{}#", block_string),
&format!("<theme>{}</theme>", theme.as_span().as_str()),
);
continue;
}
// e#hsl (hue/lit/sat) (percentage/int)#
if btype == Rule::HSL {
let which = inner.get(0).unwrap();
let value = inner.get(1).unwrap();
out = out.replace(
&format!("e#{}#", block_string),
&format!(
"<{}>{}</{}>",
which.as_span().as_str(),
value.as_span().as_str(),
which.as_span().as_str()
),
);
continue;
}
// e#html (identifier) {attrs}#
if btype == Rule::HTML {
let tag = inner.get(0).unwrap();
let attrs = inner
.iter()
.skip(1)
.into_iter()
.collect::<Vec<&Pair<'_, Rule>>>();
// build attrs string
let mut attrs_string = String::new();
for attr in attrs {
attrs_string += &format!("{} ", attr.as_span().as_str());
}
// replace
out = out.replace(
&format!("e#{}#", block_string.replace("\"", "&quot;")),
&format!("<{} {}>", tag.as_span().as_str(), attrs_string),
);
continue;
}
// e#chtml (identifier)#
if btype == Rule::CHTML {
let tag = inner.get(0).unwrap();
// replace
out = out.replace(
&format!("e#{}#", block_string),
&format!("</{}>", tag.as_span().as_str()),
);
continue;
}
// e#id (identifier)#
if btype == Rule::ID {
let id = inner.get(0).unwrap();
// replace
out = out.replace(
&format!("e#{}#", block_string),
&format!("<span id=\"{}\">", id.as_span().as_str()),
);
continue;
}
// e#class (identifier)+#
if btype == Rule::CLASS {
let attrs = inner.into_iter().collect::<Vec<Pair<'_, Rule>>>();
// build attrs string
let mut attrs_string = String::new();
for attr in attrs {
attrs_string += &format!("{} ", attr.as_span().as_str());
}
// replace
out = out.replace(
&format!("e#{}#", block_string.replace("\"", "&quot;")),
&format!("<span class=\"{}\">", attrs_string),
);
continue;
}
// e#close#
if btype == Rule::CLOSE {
// replace
out = out.replace(&format!("e#{}#", block_string), "</span>");
continue;
}
// e#animation (identifier) {attrs}#
if btype == Rule::ANIMATION {
let tag = inner.get(0).unwrap();
let attrs = inner
.iter()
.skip(1)
.into_iter()
.collect::<Vec<&Pair<'_, Rule>>>();
// build attrs string
let mut attrs_string = String::new();
for attr in attrs {
attrs_string += &format!("{} ", attr.as_span().as_str());
}
// replace
out = out.replace(
&format!("e#{}#", block_string.replace("\"", "&quot;")),
&format!("<span role=\"animation\" style=\"animation: {} {} ease-in-out forwards running; display: inline-block;\">", tag.as_span().as_str(), attrs_string),
);
continue;
}
}
// only a little bit of regex-ing remains now
// unescape arrow alignment
out = regex_replace(&out, "-&gt;&gt;", "->>");
out = regex_replace(&out, "&lt;&lt;-", "<<-");
out = regex_replace(&out, "-&gt;", "->");
out = regex_replace(&out, "&lt;-", "<-");
// allowed elements
let allowed_elements: Vec<&str> = Vec::from([
"hue", "sat", "lit", "theme", "comment", "p", "span", "style",
]);
for element in allowed_elements {
out = regex_replace(
&out,
&format!("&lt;{}&gt;", element),
&format!("<{}>", element),
);
out = regex_replace(
&out,
&format!("&lt;/{}&gt;", element),
&format!("</{}>", element),
);
}
// ssm
// essentially just ssm::parse_ssm_blocks, maybe clean this up later?
let ssm_regex = RegexBuilder::new("(ssm\\#)(?<CONTENT>.*?)\\#")
.multi_line(true)
.dot_matches_new_line(true)
.build()
.unwrap();
for capture in ssm_regex.captures_iter(&out.clone()) {
let content = capture.name("CONTENT").unwrap().as_str().replace("$", "#");
// compile
let css = crate::ssm::parse_ssm_program(content.to_string());
// replace
out = out.replace(
capture.get(0).unwrap().as_str(),
&format!("<style>{css}</style>"),
);
}
// text color (bundlrs style)
let color_regex = RegexBuilder::new("(c\\#)\\s*(?<COLOR>.*?)\\s*\\#\\s*(?<CONTENT>.*?)\\s*\\#")
.multi_line(true)
.dot_matches_new_line(true)
.build()
.unwrap();
for capture in color_regex.captures_iter(&out.clone()) {
let content = capture.name("CONTENT").unwrap().as_str();
let color = capture.name("COLOR").unwrap().as_str().replace("$", "#");
// replace
out = out.replacen(
capture.get(0).unwrap().as_str(),
&format!("<span style=\"color: {color}\">{content}</span>"),
1,
);
}
// text color thing
out = regex_replace_exp(
&out,
RegexBuilder::new(r"%(.*?)%\s*(.*?)\s*(%{2})")
.multi_line(true)
.dot_matches_new_line(true),
"<span style=\"color: $1;\" role=\"custom-color\">$2</span>",
);
// spoiler
out = regex_replace(
&out,
"(\\|\\|)\\s*(?<CONTENT>.*?)\\s*(\\|\\|)",
"<span role=\"spoiler\">$2</span>",
);
out = regex_replace(
&out,
"(\\!\\&gt;)\\s*(?<CONTENT>.*?)($|\\s\\s)",
"<span role=\"spoiler\">$2</span>",
);
// admonitions
out = regex_replace(
// title and content
&out,
"^(\\!{3})\\s(?<TYPE>.*?)\\s(?<TITLE>.+)\\n(?<CONTENT>.+)$",
"<div class=\"mdnote note-$2\">
<b class=\"mdnote-title\">$3</b>
<p>$4</p>
</div>\n",
);
out = regex_replace(
// title only
&out,
"^(\\!{3})\\s(?<TYPE>.*?)\\s(?<TITLE>.*?)$",
"<div class=\"mdnote note-$2\"><b class=\"mdnote-title\">$3</b></div>\n",
);
// highlight
out = regex_replace(
&out,
"(\\={2})(.*?)(\\={2})",
"<span class=\"highlight\">$2</span>",
);
// image with sizing
let image_sizing_regex = RegexBuilder::new("(!)\\[(.*?)\\]\\((.*?)\\)\\:\\{(.*?)x(.*?)\\}")
.multi_line(true)
.build()
.unwrap();
for capture in image_sizing_regex.captures_iter(&out.clone()) {
let title = capture.get(2).unwrap().as_str();
let src = capture.get(3).unwrap().as_str();
let width = capture.get(4).unwrap().as_str();
let height = capture.get(5).unwrap().as_str();
let result = &format!("<img alt=\"{title}\" title=\"{title}\" src=\"{src}\" style=\"width: {width}px; height: {height}px;\" />");
out = out.replacen(capture.get(0).unwrap().as_str(), result, 1);
}
// arrow alignment (flex)
let arrow_alignment_flex_regex = RegexBuilder::new("(\\->{2})(.*?)(\\->{2}|<{2}\\-)")
.multi_line(true)
.dot_matches_new_line(true)
.build()
.unwrap();
for capture in arrow_alignment_flex_regex.captures_iter(&out.clone()) {
let _match = capture.get(0).unwrap().as_str();
let content = capture.get(2).unwrap().as_str();
let align = if _match.ends_with(">") {
"right"
} else {
"center"
};
out = out.replacen(
_match,
&format!("<rf style=\"justify-content: {align}\">{content}</rf>\n"),
1,
);
}
// arrow alignment
let arrow_alignment_regex = RegexBuilder::new("(\\->{1})(.*?)(\\->{1}|<{1}\\-)")
.multi_line(true)
.dot_matches_new_line(true)
.build()
.unwrap();
for capture in arrow_alignment_regex.captures_iter(&out.clone()) {
let _match = capture.get(0).unwrap().as_str();
let content = capture.get(2).unwrap().as_str();
let align = if _match.ends_with(">") {
"right"
} else {
"center"
};
out = out.replacen(
_match,
&format!("<r style=\"text-align: {align}\">{content}</r>\n"),
1,
);
}
// return
out
}
pub fn parse_markdown(input: &str) -> String {
let tree = super::parser::parse(input);
from_tree(&tree.into_inner(), input.to_owned())
}
fn regex_replace(input: &str, pattern: &str, replace_with: &str) -> String {
RegexBuilder::new(pattern)
.multi_line(true)
.build()
.unwrap()
.replace_all(input, replace_with)
.to_string()
}
#[allow(dead_code)]
fn regex_replace_exp(input: &str, pattern: &mut RegexBuilder, replace_with: &str) -> String {
pattern
.build()
.unwrap()
.replace_all(input, replace_with)
.to_string()
}