add: near perfect metadata compatibility

This commit is contained in:
trisua 2025-07-21 22:28:43 -04:00
parent b505199492
commit f8dac8f491
8 changed files with 434 additions and 22 deletions

6
Cargo.lock generated
View file

@ -84,7 +84,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]] [[package]]
name = "attobin" name = "attobin"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"axum", "axum",
"axum-extra", "axum-extra",
@ -2586,9 +2586,9 @@ dependencies = [
[[package]] [[package]]
name = "tetratto-shared" name = "tetratto-shared"
version = "12.0.4" version = "12.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a7a1ea669f94f12da3c007012c6fbac263cfc74c964c460e19da0d9c7f6abf1" checksum = "11c2ba2be9c92a4ac566b9c3b615d7311b3d0e98b175ad84e81b44644b34dd8f"
dependencies = [ dependencies = [
"ammonia", "ammonia",
"chrono", "chrono",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "attobin" name = "attobin"
version = "0.2.0" version = "0.2.1"
edition = "2024" edition = "2024"
authors = ["trisuaso"] authors = ["trisuaso"]
repository = "https://trisua.com/t/tetratto" repository = "https://trisua.com/t/tetratto"
@ -9,7 +9,7 @@ homepage = "https://tetratto.com"
[dependencies] [dependencies]
tetratto-core = "12.0.0" tetratto-core = "12.0.0"
tetratto-shared = "12.0.4" tetratto-shared = "12.0.5"
tokio = { version = "1.46.1", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.46.1", features = ["macros", "rt-multi-thread"] }
pathbufd = "0.1.4" pathbufd = "0.1.4"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }

View file

@ -69,7 +69,7 @@ function check_message() {
if (message) { if (message) {
element.style.marginBottom = "1rem"; element.style.marginBottom = "1rem";
element.style.paddingLeft = "1rem"; element.style.paddingLeft = "1rem";
element.innerHTML += `<li class="${message_good ? "green" : "red"}">${message.replaceAll('"', "")}</li>`; element.innerHTML = `<li class="${message_good ? "green" : "red"}">${message.replaceAll('"', "")}</li>`;
} }
// clear cookies // clear cookies
@ -83,7 +83,7 @@ globalThis.show_message = (message, message_good = true) => {
const element = document.getElementById("messages"); const element = document.getElementById("messages");
element.style.marginBottom = "1rem"; element.style.marginBottom = "1rem";
element.style.paddingLeft = "1rem"; element.style.paddingLeft = "1rem";
element.innerHTML += `<li class="${message_good ? "green" : "red"}">${message.replaceAll('"', "")}</li>`; element.innerHTML = `<li class="${message_good ? "green" : "red"}">${message.replaceAll('"', "")}</li>`;
}; };
check_message(); check_message();

View file

@ -111,6 +111,11 @@ article {
} }
} }
.container {
margin: 10px auto 0;
width: 100%;
}
.content_container { .content_container {
margin: var(--pad-2) auto; margin: var(--pad-2) auto;
width: 100%; width: 100%;
@ -163,7 +168,7 @@ video {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: var(--gap-2); gap: var(--gap-2);
padding: var(--pad-2) var(--pad-4); padding: var(--pad-2) calc(var(--pad-3) * 1.5);
cursor: pointer; cursor: pointer;
background: var(--color-raised); background: var(--color-raised);
color: var(--color-text); color: var(--color-text);
@ -198,7 +203,7 @@ video {
/* input */ /* input */
input { input {
--h: 36px; --h: 36px;
padding: var(--pad-2) var(--pad-4); padding: var(--pad-2) calc(var(--pad-3) * 1.5);
background: var(--color-raised); background: var(--color-raised);
color: var(--color-text); color: var(--color-text);
outline: none; outline: none;

View file

@ -3,7 +3,11 @@
(title (title
(text "{{ entry.slug }}")) (text "{{ entry.slug }}"))
(text "{%- endif %} {{ metadata_head|safe }}") (text "{%- endif %} {{ metadata_head|safe }}")
(text "{% if metadata.page_icon|length == 0 -%}")
(link ("rel" "icon") ("href" "/public/favicon.svg")) (link ("rel" "icon") ("href" "/public/favicon.svg"))
(text "{%- endif %}")
(text "{% endblock %} {% block body %}") (text "{% endblock %} {% block body %}")
(div (div
("class" "flex flex-col gap-2") ("class" "flex flex-col gap-2")

View file

@ -1,3 +1,188 @@
use std::collections::HashSet;
pub fn render_markdown(input: &str) -> String { pub fn render_markdown(input: &str) -> String {
tetratto_shared::markdown::render_markdown(input, false) let html = tetratto_shared::markdown::render_markdown_dirty(&parse_text_color(
&parse_highlight(input),
));
let mut allowed_attributes = HashSet::new();
allowed_attributes.insert("id");
allowed_attributes.insert("class");
allowed_attributes.insert("ref");
allowed_attributes.insert("aria-label");
allowed_attributes.insert("lang");
allowed_attributes.insert("title");
allowed_attributes.insert("align");
allowed_attributes.insert("src");
allowed_attributes.insert("style");
tetratto_shared::markdown::clean_html(html, allowed_attributes)
}
fn parse_text_color_line(output: &mut String, buffer: &mut String, line: &str) {
let mut in_color_buffer = false;
let mut in_main_buffer = false;
let mut color_buffer = String::new();
let mut close_1 = false;
for char in line.chars() {
if close_1 && char != '%' {
// we expected to see another percentage to close the main buffer,
// not getting that means this wasn't meant to be a color
buffer.push('%');
in_main_buffer = false;
close_1 = false;
}
match char {
'%' => {
if in_color_buffer {
in_color_buffer = false;
in_main_buffer = true;
continue;
}
if in_main_buffer {
// ending
if !close_1 {
close_1 = true;
continue;
}
// by this point, we have: !
// %color_buffer%main_buffer%%
output.push_str(&format!(
"<span style=\"color: {color_buffer}\">{buffer}</span>\n"
));
color_buffer.clear();
buffer.clear();
// ...
in_main_buffer = false;
close_1 = false;
continue;
}
// start
// flush buffer
output.push_str(&buffer);
buffer.clear();
// toggle open
in_color_buffer = true;
}
' ' => {
if in_color_buffer == true {
buffer.push_str(&color_buffer);
color_buffer.clear();
}
buffer.push(char);
}
_ => {
if in_color_buffer {
color_buffer.push(char)
} else {
buffer.push(char)
}
}
}
}
}
fn parse_highlight_line(output: &mut String, buffer: &mut String, line: &str) {
let mut open_1 = false;
let mut open_2 = false;
let mut close_1 = false;
let mut is_open = false;
for char in line.chars() {
if close_1 && char != '=' {
buffer.push('=');
close_1 = false;
}
match char {
'=' => {
if !is_open {
// flush buffer
output.push_str(&buffer);
buffer.clear();
// toggle open
open_1 = true;
is_open = true;
} else {
if open_1 {
// this is the second open we've recieved
open_2 = true;
open_1 = false;
continue;
}
if close_1 {
// this is the second close we've received
output.push_str(&format!("<mark>{buffer}</mark>\n"));
buffer.clear();
open_1 = false;
open_2 = false;
close_1 = false;
is_open = false;
continue;
}
close_1 = true;
}
}
_ => {
if open_1 {
open_1 = false;
buffer.push('=');
}
if open_2 && is_open {
open_2 = false;
}
buffer.push(char);
}
}
}
}
/// Helper macro to quickly allow parsers to ignore fenced code blocks.
macro_rules! parser_ignores_pre {
($body:ident, $input:ident) => {{
let mut in_pre_block = false;
let mut output = String::new();
let mut buffer = String::new();
for line in $input.split("\n") {
if line.starts_with("```") {
in_pre_block = !in_pre_block;
output.push_str(&format!("{line}\n"));
continue;
}
if in_pre_block {
output.push_str(&format!("{line}\n"));
continue;
}
$body(&mut output, &mut buffer, line);
output.push_str(&format!("{buffer}\n"));
buffer.clear();
}
output
}};
}
pub fn parse_text_color(input: &str) -> String {
parser_ignores_pre!(parse_text_color_line, input)
}
pub fn parse_highlight(input: &str) -> String {
parser_ignores_pre!(parse_highlight_line, input)
} }

View file

@ -10,6 +10,7 @@ pub struct Entry {
pub created: usize, pub created: usize,
pub edited: usize, pub edited: usize,
pub content: String, pub content: String,
#[serde(default)]
pub metadata: String, pub metadata: String,
} }
@ -69,7 +70,7 @@ pub struct EntryMetadata {
#[validate(max_length = 256)] #[validate(max_length = 256)]
pub page_description: String, pub page_description: String,
/// The favicon of the page. /// The favicon of the page.
#[serde(default, alias = "PAGE_icon")] #[serde(default, alias = "PAGE_ICON")]
#[validate(max_length = 128)] #[validate(max_length = 128)]
pub page_icon: String, pub page_icon: String,
/// The title of the page shown in external embeds. /// The title of the page shown in external embeds.
@ -99,38 +100,171 @@ pub struct EntryMetadata {
#[serde(default, alias = "ACCESS_EASY_READ")] #[serde(default, alias = "ACCESS_EASY_READ")]
#[validate(max_length = 32)] #[validate(max_length = 32)]
pub access_easy_read: String, pub access_easy_read: String,
/// The color of the text in the inner container. /// The padding of the container.
#[serde(default, alias = "CONTAINER_INNER_FOREGROUND")] ///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/padding>
#[serde(default, alias = "CONTAINER_PADDING")]
#[validate(max_length = 32)] #[validate(max_length = 32)]
pub container_inner_foreground: String, pub container_padding: String,
/// The maximum width of the container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/max-width>
#[serde(default, alias = "CONTAINER_MAX_WIDTH")]
#[validate(max_length = 16)]
pub container_max_width: String,
/// The padding of the container.
/// The color of the text in the inner container.
#[serde(default, alias = "CONTAINER_INNER_FOREGROUND_COLOR")]
#[validate(max_length = 32)]
pub container_inner_foreground_color: String,
/// The background of the inner container. /// The background of the inner container.
/// ///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background> /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background>
#[serde(default, alias = "CONTAINER_INNER_BACKGROUND")] #[serde(default, alias = "CONTAINER_INNER_BACKGROUND")]
#[validate(max_length = 256)] #[validate(max_length = 256)]
pub container_inner_background: String, pub container_inner_background: String,
/// The color of the text in the outer container. /// The background of the inner container.
#[serde(default, alias = "CONTAINER_OUTER_FOREGROUND")] ///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-color>
#[serde(default, alias = "CONTAINER_INNER_BACKGROUND_COLOR")]
#[validate(max_length = 32)] #[validate(max_length = 32)]
pub container_outer_foreground: String, pub container_inner_background_color: String,
/// The background of the inner container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-image>
#[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE")]
#[validate(max_length = 128)]
pub container_inner_background_image: String,
/// The background of the inner container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-repeat>
#[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE_REPEAT")]
#[validate(max_length = 16)]
pub container_inner_background_image_repeat: String,
/// The background of the inner container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-position>
#[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE_POSITION")]
#[validate(max_length = 16)]
pub container_inner_background_image_position: String,
/// The background of the inner container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-size>
#[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE_SIZE")]
#[validate(max_length = 16)]
pub container_inner_background_image_size: String,
/// The color of the text in the outer container.
#[serde(default, alias = "CONTAINER_OUTER_FOREGROUND_COLOR")]
#[validate(max_length = 32)]
pub container_outer_foreground_color: String,
/// The background of the outer container. /// The background of the outer container.
/// ///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background> /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background>
#[serde(default, alias = "CONTAINER_OUTER_BACKGROUND")] #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND")]
#[validate(max_length = 256)] #[validate(max_length = 256)]
pub container_outer_background: String, pub container_outer_background: String,
/// The background of the outer container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-color>
#[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_COLOR")]
#[validate(max_length = 32)]
pub container_outer_background_color: String,
/// The background of the outer container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-image>
#[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE")]
#[validate(max_length = 128)]
pub container_outer_background_image: String,
/// The background of the outer container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-repeat>
#[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE_REPEAT")]
#[validate(max_length = 16)]
pub container_outer_background_image_repeat: String,
/// The background of the outer container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-position>
#[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE_POSITION")]
#[validate(max_length = 16)]
pub container_outer_background_image_position: String,
/// The background of the outer container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-size>
#[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE_SIZE")]
#[validate(max_length = 16)]
pub container_outer_background_image_size: String,
/// The border around the container. /// The border around the container.
/// ///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border> /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border>
#[serde(default, alias = "CONTAINER_BORDER")] #[serde(default, alias = "CONTAINER_BORDER")]
#[validate(max_length = 256)] #[validate(max_length = 256)]
pub container_border: String, pub container_border: String,
/// The border around the container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-image>
#[serde(default, alias = "CONTAINER_BORDER_IMAGE")]
#[validate(max_length = 128)]
pub container_border_image: String,
/// The border around the container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-image-slice>
#[serde(default, alias = "CONTAINER_BORDER_IMAGE_SLICE")]
#[validate(max_length = 16)]
pub container_border_image_slice: String,
/// The border around the container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-image-width>
#[serde(default, alias = "CONTAINER_BORDER_IMAGE_WIDTH")]
#[validate(max_length = 16)]
pub container_border_image_width: String,
/// The border around the container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-image-outset>
#[serde(default, alias = "CONTAINER_BORDER_IMAGE_OUTSET")]
#[validate(max_length = 32)]
pub container_border_image_outset: String,
/// The border around the container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-image-repeat>
#[serde(default, alias = "CONTAINER_BORDER_IMAGE_REPEAT")]
#[validate(max_length = 16)]
pub container_border_image_repeat: String,
/// The border around the container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-color>
#[serde(default, alias = "CONTAINER_BORDER_COLOR")]
#[validate(max_length = 32)]
pub container_border_color: String,
/// The border around the container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-width>
#[serde(default, alias = "CONTAINER_BORDER_WIDTH")]
#[validate(max_length = 16)]
pub container_border_width: String,
/// The border around the container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-style>
#[serde(default, alias = "CONTAINER_BORDER_STYLE")]
#[validate(max_length = 16)]
pub container_border_style: String,
/// The border around the container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius>
#[serde(default, alias = "CONTAINER_BORDER_RADIUS")]
#[validate(max_length = 16)]
pub container_border_radius: String,
/// The shadow around the container. /// The shadow around the container.
/// ///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow> /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow>
#[serde(default, alias = "CONTAINER_SHADOW")] #[serde(default, alias = "CONTAINER_SHADOW")]
#[validate(max_length = 32)] #[validate(max_length = 32)]
pub container_shadow: String, pub container_shadow: String,
/// The shadow under text.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/text-shadow>
#[serde(default, alias = "CONTENT_TEXT_SHADOW")]
#[validate(max_length = 32)]
pub content_text_shadow: String,
/// The name of a font from Google Fonts to use. /// The name of a font from Google Fonts to use.
#[serde(default, alias = "CONTENT_FONT")] #[serde(default, alias = "CONTENT_FONT")]
#[validate(max_length = 32)] #[validate(max_length = 32)]
@ -166,6 +300,17 @@ macro_rules! metadata_css {
)); ));
} }
}; };
($selector:literal, $property:literal, $format:literal, $self:ident.$field:ident->$output:ident) => {
if !$self.$field.is_empty() {
$output.push_str(&format!(
"{} {{ {}: {}; }}\n",
$selector,
$property,
format!($format, EntryMetadata::css_escape(&$self.$field))
));
}
};
} }
impl EntryMetadata { impl EntryMetadata {
@ -233,12 +378,34 @@ impl EntryMetadata {
pub fn css(&self) -> String { pub fn css(&self) -> String {
let mut output = "<style>".to_string(); let mut output = "<style>".to_string();
metadata_css!(".container", "color", self.container_inner_foreground->output); metadata_css!(".container", "padding", self.container_padding->output);
metadata_css!(".container", "max-width", self.container_max_width->output);
metadata_css!(".container", "color", self.container_inner_foreground_color->output);
metadata_css!(".container", "background", self.container_inner_background->output); metadata_css!(".container", "background", self.container_inner_background->output);
metadata_css!("body", "color", self.container_outer_foreground->output); metadata_css!(".container", "background-color", self.container_inner_background_color->output);
metadata_css!(".container", "background-image", "url('{}')", self.container_inner_background_image->output);
metadata_css!(".container", "background-repeat", self.container_inner_background_image_repeat->output);
metadata_css!(".container", "background-position", self.container_inner_background_image_position->output);
metadata_css!(".container", "background-size", self.container_inner_background_image_size->output);
metadata_css!("body", "color", self.container_outer_foreground_color->output);
metadata_css!("body", "background", self.container_outer_background->output); metadata_css!("body", "background", self.container_outer_background->output);
metadata_css!("body", "background-color", self.container_outer_background_color->output);
metadata_css!("body", "background-image", "url('{}')", self.container_outer_background_image->output);
metadata_css!("body", "background-repeat", self.container_outer_background_image_repeat->output);
metadata_css!("body", "background-position", self.container_outer_background_image_position->output);
metadata_css!("body", "background-size", self.container_outer_background_image_size->output);
metadata_css!(".container", "border", self.container_border->output); metadata_css!(".container", "border", self.container_border->output);
metadata_css!(".container", "border-image", "url('{}')", self.container_border_image->output);
metadata_css!(".container", "border-image-slice", self.container_border_image_slice->output);
metadata_css!(".container", "border-image-repeat", self.container_border_image_repeat->output);
metadata_css!(".container", "border-image-outset", self.container_border_image_outset->output);
metadata_css!(".container", "border-image-width", self.container_border_image_width->output);
metadata_css!(".container", "border-color", self.container_border_color->output);
metadata_css!(".container", "border-style", self.container_border_style->output);
metadata_css!(".container", "border-width", self.container_border_width->output);
metadata_css!(".container", "border-radius", self.container_border_radius->output);
metadata_css!(".container", "box-shadow", self.container_shadow->output); metadata_css!(".container", "box-shadow", self.container_shadow->output);
metadata_css!(".container", "text-shadow", self.content_text_shadow->output);
metadata_css!(".container a", "color", self.content_text_link_color->output); metadata_css!(".container a", "color", self.content_text_link_color->output);
if self.content_text_align != TextAlignment::Left { if self.content_text_align != TextAlignment::Left {
@ -272,4 +439,54 @@ impl EntryMetadata {
output + "</style>" output + "</style>"
} }
pub fn ini_to_toml(input: &str) -> String {
let mut output = String::new();
for line in input.split("\n") {
if !line.contains("=") {
// no equal sign, skip line (some other toml function)
continue;
}
let mut key = String::new();
let mut value = String::new();
let mut chars = line.chars();
let mut in_value = false;
while let Some(char) = chars.next() {
if !in_value {
key.push(char);
if char == '=' {
in_value = true;
}
continue;
} else {
value.push(char);
continue;
}
}
value = value.trim().to_string();
// determine if we need to stringify
let mut is_numeric = false;
for char in value.chars() {
is_numeric = char.is_numeric();
}
if !is_numeric && !value.starts_with("[") && !value.starts_with("\"") {
value = format!("\"{value}\"");
}
// push line
output.push_str(&format!("{key} {value}\n"));
}
output
}
} }

View file

@ -104,7 +104,8 @@ async fn view_request(
}; };
// check metadata // check metadata
let metadata: EntryMetadata = match toml::from_str(&entry.metadata) { let metadata: EntryMetadata = match toml::from_str(&EntryMetadata::ini_to_toml(&entry.metadata))
{
Ok(x) => x, Ok(x) => x,
Err(e) => { Err(e) => {
let mut ctx = default_context(&data, &build_code); let mut ctx = default_context(&data, &build_code);
@ -269,7 +270,7 @@ async fn create_request(
} }
// check metadata // check metadata
let metadata: EntryMetadata = match toml::from_str(&req.metadata) { let metadata: EntryMetadata = match toml::from_str(&EntryMetadata::ini_to_toml(&req.metadata)) {
Ok(x) => x, Ok(x) => x,
Err(e) => return Json(Error::MiscError(e.to_string()).into()), Err(e) => return Json(Error::MiscError(e.to_string()).into()),
}; };
@ -359,7 +360,7 @@ async fn edit_request(
} }
// check metadata // check metadata
let metadata: EntryMetadata = match toml::from_str(&req.metadata) { let metadata: EntryMetadata = match toml::from_str(&EntryMetadata::ini_to_toml(&req.metadata)) {
Ok(x) => x, Ok(x) => x,
Err(e) => return Json(Error::MiscError(e.to_string()).into()), Err(e) => return Json(Error::MiscError(e.to_string()).into()),
}; };