diff --git a/Cargo.lock b/Cargo.lock
index b4f4e6a..2f7bf7d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -92,6 +92,7 @@ dependencies = [
"glob",
"nanoneo",
"pathbufd",
+ "regex",
"serde",
"serde_json",
"serde_valid",
diff --git a/Cargo.toml b/Cargo.toml
index 1e0be95..0f1e264 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,3 +30,4 @@ glob = "0.3.2"
serde_json = "1.0.141"
toml = "0.9.2"
serde_valid = { version = "1.0.5", features = ["toml"] }
+regex = "1.11.1"
diff --git a/app/public/style.css b/app/public/style.css
index 486357b..a61c225 100644
--- a/app/public/style.css
+++ b/app/public/style.css
@@ -111,7 +111,7 @@ article {
}
}
-.container {
+.container:not(#preview_tab) {
margin: 10px auto 0;
width: 100%;
}
@@ -322,6 +322,10 @@ hr.margin {
margin: var(--pad-4) 0;
}
+span.img_sizer {
+ display: block;
+}
+
p,
li,
span,
diff --git a/src/markdown.rs b/src/markdown.rs
index 24b1bd5..62ba846 100644
--- a/src/markdown.rs
+++ b/src/markdown.rs
@@ -2,7 +2,7 @@ use std::collections::HashSet;
pub fn render_markdown(input: &str) -> String {
let html = tetratto_shared::markdown::render_markdown_dirty(&parse_text_color(
- &parse_highlight(input),
+ &parse_highlight(&parse_underline(&parse_comment(&parse_image_size(input)))),
));
let mut allowed_attributes = HashSet::new();
@@ -151,6 +151,235 @@ fn parse_highlight_line(output: &mut String, buffer: &mut String, line: &str) {
}
}
+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;
+ }
+
+ 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 size_lhs == "auto" {
+ size_lhs
+ } else {
+ format!("{size_lhs}px")
+ },
+ if size_rhs == "auto" {
+ size_rhs
+ } else {
+ format!("{size_rhs}px")
+ }
+ ));
+
+ 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)
+ }
+ }
+ }
+ }
+}
+
/// Helper macro to quickly allow parsers to ignore fenced code blocks.
macro_rules! parser_ignores_pre {
($body:ident, $input:ident) => {{
@@ -186,3 +415,15 @@ pub fn parse_text_color(input: &str) -> String {
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)
+}
diff --git a/src/model.rs b/src/model.rs
index 5deb76c..8beb0fb 100644
--- a/src/model.rs
+++ b/src/model.rs
@@ -363,8 +363,8 @@ impl EntryMetadata {
output.push_str(&format!(
"
- ",
- self.content_font.replace(" ", "+"),
+ ",
+ self.content_font.replace(" ", "+").replace(" ", "_"),
));
}
diff --git a/src/routes.rs b/src/routes.rs
index 135a23b..6a42fb1 100644
--- a/src/routes.rs
+++ b/src/routes.rs
@@ -23,6 +23,8 @@ use tetratto_shared::{
unix_epoch_timestamp,
};
+pub const NAME_REGEX: &str = r"[^\w_\-\.,!]+";
+
pub fn routes() -> Router {
Router::new()
.nest_service(
@@ -269,6 +271,16 @@ async fn create_request(
return Json(Error::DataTooLong("content".to_string()).into());
}
+ // check slug
+ let regex = regex::RegexBuilder::new(NAME_REGEX)
+ .multi_line(true)
+ .build()
+ .unwrap();
+
+ if regex.captures(&req.slug).is_some() {
+ return Json(Error::MiscError("This slug contains invalid characters".to_string()).into());
+ }
+
// check metadata
let metadata: EntryMetadata = match toml::from_str(&EntryMetadata::ini_to_toml(&req.metadata)) {
Ok(x) => x,
@@ -428,6 +440,18 @@ async fn edit_request(
return Json(Error::DataTooLong("slug".to_string()).into());
}
+ // check slug
+ let regex = regex::RegexBuilder::new(NAME_REGEX)
+ .multi_line(true)
+ .build()
+ .unwrap();
+
+ if regex.captures(&new_slug).is_some() {
+ return Json(
+ Error::MiscError("This slug contains invalid characters".to_string()).into(),
+ );
+ }
+
// check for existing
if data
.query(&SimplifiedQuery {