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 {