add: parse_image_line, parse_link_line

This commit is contained in:
trisua 2025-07-24 01:06:03 -04:00
parent d8167aa06f
commit 9fe5735127
9 changed files with 234 additions and 25 deletions

4
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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();

View file

@ -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%;
}

View file

@ -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")))

View file

@ -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")))

View file

@ -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("<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) {
@ -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!(
"<span style=\"width: {}; height: {}\" class=\"img_sizer\">![{buffer}</span>",
if size_lhs == "auto" {
size_lhs
} else {
format!("{size_lhs}px")
},
if size_rhs == "auto" {
size_rhs
} else {
format!("{size_rhs}px")
}
"<span style=\"width: {size_lhs}; height: {size_rhs}\" class=\"img_sizer\">![{buffer}</span>"
));
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!(
"<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) => {{
@ -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)
}

View file

@ -364,7 +364,7 @@ impl EntryMetadata {
"<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">
<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>
<link href=\"https://fonts.googleapis.com/css2?family={}&display=swap\" rel=\"stylesheet\">",
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("_", " ")
));
}

View file

@ -208,10 +208,20 @@ async fn editor_request(
#[derive(Deserialize)]
struct RenderMarkdown {
content: String,
metadata: String,
}
async fn render_request(Json(req): Json<RenderMarkdown>) -> 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(