From 9fe5735127d3b75b8366dc69ffdd11f13cd87169 Mon Sep 17 00:00:00 2001 From: trisua Date: Thu, 24 Jul 2025 01:06:03 -0400 Subject: [PATCH] add: parse_image_line, parse_link_line --- Cargo.lock | 4 +- Cargo.toml | 2 +- app/public/app.js | 9 ++ app/public/style.css | 4 +- app/templates_src/edit.lisp | 5 +- app/templates_src/index.lisp | 5 +- src/markdown.rs | 205 ++++++++++++++++++++++++++++++++--- src/model.rs | 13 ++- src/routes.rs | 12 +- 9 files changed, 234 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f7bf7d..079561e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2551,9 +2551,9 @@ dependencies = [ [[package]] name = "tetratto-core" -version = "12.0.0" +version = "12.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2446a581866039e72a9203a472b97390991cb2ef51c5c29dad3aa69dd41edc2d" +checksum = "2c1b01499f7471ee6f05299c06ebb18760440452714c6f9a6c0c9e0cf9a663bd" dependencies = [ "async-recursion", "base16ct", diff --git a/Cargo.toml b/Cargo.toml index 0f1e264..de66da2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "AGPL-3.0-or-later" homepage = "https://tetratto.com" [dependencies] -tetratto-core = "12.0.0" +tetratto-core = "12.0.1" tetratto-shared = "12.0.5" tokio = { version = "1.46.1", features = ["macros", "rt-multi-thread"] } pathbufd = "0.1.4" diff --git a/app/public/app.js b/app/public/app.js index 987273b..c5305b3 100644 --- a/app/public/app.js +++ b/app/public/app.js @@ -144,6 +144,14 @@ globalThis.tab_editor = () => { }; globalThis.tab_preview = async () => { + if ( + !document + .getElementById("preview_tab_button") + .classList.contains("camo") + ) { + return; + } + // render const res = await ( await fetch("/api/v1/render", { @@ -151,6 +159,7 @@ globalThis.tab_preview = async () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: globalThis.editor.getValue(), + metadata: globalThis.metadata_editor.getValue(), }), }) ).text(); diff --git a/app/public/style.css b/app/public/style.css index a61c225..6fd2537 100644 --- a/app/public/style.css +++ b/app/public/style.css @@ -111,7 +111,7 @@ article { } } -.container:not(#preview_tab) { +.container:not(#preview_tab):not(#tabs_group) { margin: 10px auto 0; width: 100%; } @@ -375,7 +375,7 @@ h6 { h1 { text-align: center; - margin-bottom: 2rem; + margin: 2rem 0; width: 100%; } diff --git a/app/templates_src/edit.lisp b/app/templates_src/edit.lisp index 9de0787..1840dcd 100644 --- a/app/templates_src/edit.lisp +++ b/app/templates_src/edit.lisp @@ -21,13 +21,14 @@ ("onclick" "tab_metadata()") (text "Metadata"))) (div - ("class" "card tab tabs") + ("class" "card tab tabs container") + ("id" "tabs_group") (div ("id" "editor_tab") ("class" "tab fadein")) (div ("id" "preview_tab") - ("class" "tab fadein hidden container")) + ("class" "tab fadein hidden")) (div ("id" "metadata_tab") ("class" "tab fadein hidden"))) diff --git a/app/templates_src/index.lisp b/app/templates_src/index.lisp index f5ee66f..f928864 100644 --- a/app/templates_src/index.lisp +++ b/app/templates_src/index.lisp @@ -21,13 +21,14 @@ ("onclick" "tab_metadata()") (text "Metadata"))) (div - ("class" "card tab tabs") + ("class" "card tab tabs container") + ("id" "tabs_group") (div ("id" "editor_tab") ("class" "tab fadein")) (div ("id" "preview_tab") - ("class" "tab fadein hidden container")) + ("class" "tab fadein hidden")) (div ("id" "metadata_tab") ("class" "tab fadein hidden"))) diff --git a/src/markdown.rs b/src/markdown.rs index 62ba846..e209cd2 100644 --- a/src/markdown.rs +++ b/src/markdown.rs @@ -2,7 +2,9 @@ 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_underline(&parse_comment(&parse_image_size(input)))), + &parse_highlight(&parse_link(&parse_image(&parse_image_size( + &parse_underline(&parse_comment(input)), + )))), )); let mut allowed_attributes = HashSet::new(); @@ -16,7 +18,13 @@ pub fn render_markdown(input: &str) -> String { allowed_attributes.insert("src"); allowed_attributes.insert("style"); - tetratto_shared::markdown::clean_html(html, allowed_attributes) + 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) { @@ -258,6 +266,11 @@ fn parse_comment_line(output: &mut String, _: &mut String, line: &str) { return; } + if line == "[..]" { + output.push_str(" "); + return; + } + output.push_str(line); } @@ -323,17 +336,7 @@ fn parse_image_size_line(output: &mut String, buffer: &mut String, line: &str) { 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") - } + "![{buffer}" )); size_lhs = String::new(); @@ -380,6 +383,174 @@ fn parse_image_size_line(output: &mut String, buffer: &mut String, line: &str) { } } +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}\"" + )); + + 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) => {{ @@ -427,3 +598,11 @@ pub fn parse_comment(input: &str) -> String { 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) +} diff --git a/src/model.rs b/src/model.rs index 8beb0fb..c743fc1 100644 --- a/src/model.rs +++ b/src/model.rs @@ -364,7 +364,7 @@ impl EntryMetadata { " ", - self.content_font.replace(" ", "+").replace(" ", "_"), + self.content_font.replace(" ", "+").replace("_", "+"), )); } @@ -416,6 +416,15 @@ impl EntryMetadata { } for (element, size) in &self.content_text_size { + if element == "*" { + output.push_str(&format!( + ".container, .container * {{ font-size: {}; }}\n", + EntryMetadata::css_escape(&size) + )); + + continue; + } + output.push_str(&format!( ".container {} {{ font-size: {}; }}\n", element, @@ -426,7 +435,7 @@ impl EntryMetadata { if !self.content_font.is_empty() { output.push_str(&format!( ".container {{ font-family: \"{}\", system-ui; }}", - self.content_font + self.content_font.replace("_", " ") )); } diff --git a/src/routes.rs b/src/routes.rs index 6a42fb1..e4db1e5 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -208,10 +208,20 @@ async fn editor_request( #[derive(Deserialize)] struct RenderMarkdown { content: String, + metadata: String, } async fn render_request(Json(req): Json) -> impl IntoResponse { - crate::markdown::render_markdown(&req.content) + let metadata: EntryMetadata = match toml::from_str(&EntryMetadata::ini_to_toml(&req.metadata)) { + Ok(x) => x, + Err(e) => return Html(e.to_string()), + }; + + if let Err(e) = metadata.validate() { + return Html(e.to_string()); + } + + Html(crate::markdown::render_markdown(&req.content) + &metadata.css()) } async fn exists_request(